mbeditor 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +191 -0
  3. data/README.md +226 -3
  4. data/app/assets/javascripts/mbeditor/application.js +5 -0
  5. data/app/assets/javascripts/mbeditor/application_iife_tail.js +6 -0
  6. data/app/assets/javascripts/mbeditor/collaboration_identity.js +234 -0
  7. data/app/assets/javascripts/mbeditor/collaboration_service.js +690 -0
  8. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +120 -19
  9. data/app/assets/javascripts/mbeditor/components/FileTree.js +127 -8
  10. data/app/assets/javascripts/mbeditor/components/GitPanel.js +12 -3
  11. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +127 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +948 -72
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +565 -0
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +130 -10
  15. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +1 -0
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +4 -2
  17. data/app/assets/javascripts/mbeditor/editor_plugins.js +661 -140
  18. data/app/assets/javascripts/mbeditor/file_import.js +146 -0
  19. data/app/assets/javascripts/mbeditor/file_service.js +68 -3
  20. data/app/assets/javascripts/mbeditor/tab_manager.js +50 -1
  21. data/app/assets/javascripts/mbeditor/websocket_service.js +89 -0
  22. data/app/assets/stylesheets/mbeditor/editor.css +273 -10
  23. data/app/channels/mbeditor/channel_authentication.rb +94 -0
  24. data/app/channels/mbeditor/collaboration_channel.rb +84 -0
  25. data/app/channels/mbeditor/editor_channel.rb +40 -1
  26. data/app/controllers/mbeditor/application_controller.rb +5 -1
  27. data/app/controllers/mbeditor/editors_controller.rb +481 -19
  28. data/app/controllers/mbeditor/git_controller.rb +9 -2
  29. data/app/services/mbeditor/availability_probe.rb +76 -17
  30. data/app/services/mbeditor/code_search_service.rb +23 -3
  31. data/app/services/mbeditor/collaboration_doc_store.rb +116 -0
  32. data/app/services/mbeditor/file_import_service.rb +103 -0
  33. data/app/services/mbeditor/git_combined_diff_service.rb +36 -5
  34. data/app/services/mbeditor/git_info_service.rb +6 -0
  35. data/app/services/mbeditor/git_service.rb +22 -6
  36. data/app/services/mbeditor/js_globals_service.rb +31 -2
  37. data/app/services/mbeditor/js_program_service.rb +173 -0
  38. data/app/services/mbeditor/lsp_diagnostics_translator.rb +99 -5
  39. data/app/services/mbeditor/model_graph_service.rb +232 -0
  40. data/app/services/mbeditor/presence_registry.rb +83 -0
  41. data/app/services/mbeditor/ri_definition_service.rb +39 -5
  42. data/app/services/mbeditor/search_replace_service.rb +24 -4
  43. data/app/views/layouts/mbeditor/application.html.erb +2 -0
  44. data/lib/mbeditor/configuration.rb +43 -3
  45. data/lib/mbeditor/engine.rb +34 -0
  46. data/lib/mbeditor/exception_log.rb +84 -0
  47. data/lib/mbeditor/route_map.rb +6 -0
  48. data/lib/mbeditor/ruby_lsp_client.rb +28 -1
  49. data/lib/mbeditor/version.rb +1 -1
  50. data/lib/mbeditor.rb +1 -0
  51. data/vendor/assets/javascripts/yjs-collab.js +12 -0
  52. metadata +16 -2
@@ -10,6 +10,19 @@
10
10
  var ProblemsPanel = (function () {
11
11
  var SEVERITY_ERROR = 8;
12
12
  var SEVERITY_WARNING = 4;
13
+ var SEVERITY_INFO = 2;
14
+ var SEVERITY_HINT = 1;
15
+
16
+ // Info and Hint share a row style: both mean "worth knowing, not worth
17
+ // stopping for", and two shades of grey would be a distinction without a
18
+ // difference in a list this dense.
19
+ var SEVERITY_KIND = { 8: 'error', 4: 'warning', 2: 'info', 1: 'info' };
20
+ var SEVERITY_ICON = {
21
+ error: 'fa-bug',
22
+ warning: 'fa-exclamation-triangle',
23
+ info: 'fa-info-circle'
24
+ };
25
+ var SEVERITY_LABEL = { error: 'Error', warning: 'Warning', info: 'Info' };
13
26
  // Long enough to recognise the statement, short enough not to push the
14
27
  // location off the end of the row.
15
28
  var CODE_PREVIEW_LIMIT = 120;
@@ -28,10 +41,14 @@ var ProblemsPanel = (function () {
28
41
  // counts share this one pass. Exposed on the component for the status bar.
29
42
  function collect() {
30
43
  var monaco = window.monaco;
31
- if (!monaco || !monaco.editor) return { errors: [], warnings: [], byFile: [] };
44
+ if (!monaco || !monaco.editor) return { errors: [], warnings: [], infos: [], byFile: [] };
32
45
 
33
46
  var errors = [];
34
47
  var warnings = [];
48
+ // RuboCop's convention and refactor offenses grade as Info, and its own
49
+ // `info` level as Hint. Listing only errors and warnings would hide the
50
+ // majority of a lint run.
51
+ var infos = [];
35
52
  var byFile = [];
36
53
 
37
54
  monaco.editor.getModels().forEach(function (model) {
@@ -39,7 +56,8 @@ var ProblemsPanel = (function () {
39
56
  if (!path || model.isDisposed()) return;
40
57
 
41
58
  var markers = monaco.editor.getModelMarkers({ resource: model.uri }).filter(function (m) {
42
- return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING;
59
+ return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING ||
60
+ m.severity === SEVERITY_INFO || m.severity === SEVERITY_HINT;
43
61
  });
44
62
  if (markers.length === 0) return;
45
63
 
@@ -48,7 +66,10 @@ var ProblemsPanel = (function () {
48
66
  // Carry the source line alongside the marker: read here, while the model
49
67
  // is in hand, rather than looking the model up again at render time.
50
68
  var entries = markers.map(function (m) {
51
- (m.severity === SEVERITY_ERROR ? errors : warnings).push(m);
69
+ var bucket = m.severity === SEVERITY_ERROR ? errors
70
+ : m.severity === SEVERITY_WARNING ? warnings
71
+ : infos;
72
+ bucket.push(m);
52
73
  return { marker: m, code: codePreview(model, m.startLineNumber) };
53
74
  });
54
75
 
@@ -56,9 +77,11 @@ var ProblemsPanel = (function () {
56
77
  });
57
78
 
58
79
  byFile.sort(function (a, b) { return a.path < b.path ? -1 : a.path > b.path ? 1 : 0; });
59
- return { errors: errors, warnings: warnings, byFile: byFile };
80
+ return { errors: errors, warnings: warnings, infos: infos, byFile: byFile };
60
81
  }
61
82
 
83
+ // The status bar deliberately tallies only errors and warnings: a count that
84
+ // included every convention offense would be a number nobody acts on.
62
85
  function counts() {
63
86
  var all = collect();
64
87
  return { errors: all.errors.length, warnings: all.warnings.length };
@@ -73,6 +96,38 @@ var ProblemsPanel = (function () {
73
96
  var _filter = React.useState('');
74
97
  var filter = _filter[0], setFilter = _filter[1];
75
98
 
99
+ // Exceptions raised by the host app. Unlike markers these are not per-model
100
+ // — a runtime failure isn't a property of a file you happen to have open —
101
+ // so they live in their own section above the marker list.
102
+ var _exceptions = React.useState([]);
103
+ var exceptions = _exceptions[0], setExceptions = _exceptions[1];
104
+
105
+ React.useEffect(function () {
106
+ var cancelled = false;
107
+ if (FileService.getExceptions) {
108
+ FileService.getExceptions().then(function (data) {
109
+ if (!cancelled) setExceptions((data && data.exceptions) || []);
110
+ })["catch"](function () { /* older host, or capture disabled */ });
111
+ }
112
+
113
+ // Live push while the panel is open.
114
+ var onException = function (e) {
115
+ setExceptions(function (prev) {
116
+ return [e.detail].concat(prev).slice(0, 50);
117
+ });
118
+ };
119
+ window.addEventListener('mbeditor:exception', onException);
120
+ return function () {
121
+ cancelled = true;
122
+ window.removeEventListener('mbeditor:exception', onException);
123
+ };
124
+ }, []);
125
+
126
+ var clearExceptions = function () {
127
+ setExceptions([]);
128
+ if (FileService.clearExceptions) FileService.clearExceptions()["catch"](function () {});
129
+ };
130
+
76
131
  var MIN_HEIGHT = 120;
77
132
  var _height = React.useState(function () {
78
133
  var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
@@ -122,7 +177,7 @@ var ProblemsPanel = (function () {
122
177
  }).filter(function (entry) { return entry.markers.length > 0; })
123
178
  : problems.byFile;
124
179
 
125
- var total = problems.errors.length + problems.warnings.length;
180
+ var total = problems.errors.length + problems.warnings.length + problems.infos.length;
126
181
 
127
182
  return React.createElement(
128
183
  'div',
@@ -142,6 +197,7 @@ var ProblemsPanel = (function () {
142
197
  { className: 'ide-problems-summary' },
143
198
  problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
144
199
  ', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
200
+ ', ' + problems.infos.length + ' info' +
145
201
  ' in open files'
146
202
  ),
147
203
  React.createElement('input', {
@@ -159,7 +215,57 @@ var ProblemsPanel = (function () {
159
215
  React.createElement(
160
216
  'div',
161
217
  { className: 'ide-problems-body' },
162
- total === 0
218
+ exceptions.length > 0 && React.createElement(
219
+ 'div',
220
+ { className: 'ide-problems-file ide-problems-exceptions' },
221
+ React.createElement(
222
+ 'div',
223
+ { className: 'ide-problems-file-name' },
224
+ React.createElement('i', { className: 'fas fa-bolt', 'aria-hidden': 'true' }),
225
+ ' Exceptions',
226
+ React.createElement('span', { className: 'ide-problems-file-count' }, exceptions.length),
227
+ React.createElement('button', {
228
+ type: 'button', className: 'ide-problems-btn ide-problems-clear',
229
+ title: 'Clear recorded exceptions', onClick: clearExceptions
230
+ }, React.createElement('i', { className: 'fas fa-times' }))
231
+ ),
232
+ exceptions.map(function (ex) {
233
+ return React.createElement(
234
+ 'div',
235
+ { className: 'ide-problems-exception', key: ex.id },
236
+ React.createElement(
237
+ 'div',
238
+ { className: 'ide-problems-item ide-problems-item-error ide-problems-exception-head' },
239
+ React.createElement('i', { className: 'fas fa-bolt ide-problems-icon', 'aria-hidden': 'true' }),
240
+ React.createElement('span', { className: 'ide-problems-msg' }, ex.klass + ': ' + ex.message),
241
+ (ex.controller || ex.path) && React.createElement(
242
+ 'span',
243
+ { className: 'ide-problems-source' },
244
+ ex.controller ? ex.controller + '#' + ex.action : ex.path
245
+ )
246
+ ),
247
+ // Only workspace frames survive the server side, so every one of
248
+ // these is a file this editor can actually open.
249
+ (ex.frames || []).map(function (f, i) {
250
+ return React.createElement(
251
+ 'button',
252
+ {
253
+ type: 'button',
254
+ className: 'ide-problems-item ide-problems-frame',
255
+ key: ex.id + ':' + i,
256
+ 'aria-label': 'Open ' + f.file + ' line ' + f.line,
257
+ onClick: function () { if (onOpenFile) onOpenFile(f.file, f.line, 1); }
258
+ },
259
+ React.createElement('span', { className: 'ide-problems-code' }, f.file),
260
+ React.createElement('span', { className: 'ide-problems-loc' }, '[' + f.line + ']')
261
+ );
262
+ })
263
+ );
264
+ })
265
+ ),
266
+ total === 0 && exceptions.length > 0
267
+ ? null
268
+ : total === 0
163
269
  ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
164
270
  : shown.length === 0
165
271
  ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems match the filter')
@@ -175,14 +281,17 @@ var ProblemsPanel = (function () {
175
281
  ),
176
282
  entry.markers.map(function (item, index) {
177
283
  var marker = item.marker;
178
- var isError = marker.severity === SEVERITY_ERROR;
284
+ var kind = SEVERITY_KIND[marker.severity] || 'info';
285
+ var docsHref = marker.code && marker.code.target
286
+ ? String(marker.code.target)
287
+ : null;
179
288
  return React.createElement(
180
289
  'button',
181
290
  {
182
291
  type: 'button',
183
- className: 'ide-problems-item ide-problems-item-' + (isError ? 'error' : 'warning'),
292
+ className: 'ide-problems-item ide-problems-item-' + kind,
184
293
  key: entry.path + ':' + marker.startLineNumber + ':' + index,
185
- 'aria-label': (isError ? 'Error' : 'Warning') + ': ' + marker.message +
294
+ 'aria-label': SEVERITY_LABEL[kind] + ': ' + marker.message +
186
295
  ', ' + entry.path + ' line ' + marker.startLineNumber +
187
296
  (item.code ? ', source: ' + item.code : ''),
188
297
  onClick: function () {
@@ -190,11 +299,22 @@ var ProblemsPanel = (function () {
190
299
  }
191
300
  },
192
301
  React.createElement('i', {
193
- className: 'fas ' + (isError ? 'fa-bug' : 'fa-exclamation-triangle') + ' ide-problems-icon',
302
+ className: 'fas ' + SEVERITY_ICON[kind] + ' ide-problems-icon',
194
303
  'aria-hidden': 'true'
195
304
  }),
196
305
  React.createElement('span', { className: 'ide-problems-msg' }, marker.message),
197
306
  item.code && React.createElement('code', { className: 'ide-problems-code' }, item.code),
307
+ // RuboCop ships a docs URL per cop; when the marker carries
308
+ // one, the cop name becomes a link out to it. stopPropagation
309
+ // so following it doesn't also jump the editor to the offense.
310
+ docsHref && React.createElement('a', {
311
+ className: 'ide-problems-docs',
312
+ href: docsHref,
313
+ target: '_blank',
314
+ rel: 'noopener noreferrer',
315
+ title: 'Documentation for ' + marker.code.value,
316
+ onClick: function (e) { e.stopPropagation(); }
317
+ }, marker.code.value),
198
318
  marker.source && React.createElement('span', { className: 'ide-problems-source' }, marker.source),
199
319
  React.createElement(
200
320
  'span',
@@ -102,6 +102,7 @@ var ShortcutHelp = function ShortcutHelp(_ref) {
102
102
  'tbody',
103
103
  null,
104
104
  React.createElement(Row, { keys: 'Ctrl+P', desc: 'Quick-open any file by name' }),
105
+ React.createElement(Row, { keys: 'PgUp/PgDn', desc: 'Cycle between cursor position and last jump origin' }),
105
106
  React.createElement(Row, { keys: 'Ctrl+S', desc: 'Save the active file' }),
106
107
  React.createElement(Row, { keys: 'Ctrl+Shift+G', desc: 'Toggle git panel' }),
107
108
  React.createElement(Row, { keys: 'Ctrl+Shift+L', desc: 'Toggle Rails log panel' }),
@@ -60,9 +60,11 @@ var TabBar = function TabBar(_ref) {
60
60
  });
61
61
  if (hasError) return 'tab-has-error';
62
62
 
63
+ // Only real warnings tint the tab. Convention and refactor offenses grade
64
+ // as info/hint and are far too common to be worth an amber tab — a tint
65
+ // that's always on tells you nothing.
63
66
  var hasWarning = tabMarkers.some(function (marker) {
64
- var severity = String(marker && marker.severity || '').toLowerCase();
65
- return severity !== 'error' && severity !== 'fatal';
67
+ return String(marker && marker.severity || '').toLowerCase() === 'warning';
66
68
  });
67
69
  if (hasWarning) return 'tab-has-warning';
68
70