mbeditor 0.12.8 → 0.13.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/app/assets/javascripts/mbeditor/application.js +7 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
  5. data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
  6. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +135 -40
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
  9. data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
  10. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
  11. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +629 -137
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
  15. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
  17. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
  18. data/app/assets/javascripts/mbeditor/editor_plugins.js +444 -116
  19. data/app/assets/javascripts/mbeditor/file_import.js +78 -0
  20. data/app/assets/javascripts/mbeditor/file_service.js +122 -18
  21. data/app/assets/javascripts/mbeditor/git_service.js +130 -8
  22. data/app/assets/javascripts/mbeditor/history_service.js +33 -38
  23. data/app/assets/javascripts/mbeditor/log_service.js +4 -0
  24. data/app/assets/javascripts/mbeditor/search_service.js +30 -2
  25. data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
  26. data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
  27. data/app/assets/stylesheets/mbeditor/editor.css +302 -55
  28. data/app/channels/mbeditor/channel_authentication.rb +7 -0
  29. data/app/channels/mbeditor/editor_channel.rb +6 -3
  30. data/app/controllers/mbeditor/application_controller.rb +5 -0
  31. data/app/controllers/mbeditor/editors_controller.rb +173 -29
  32. data/app/controllers/mbeditor/git_controller.rb +3 -3
  33. data/app/controllers/mbeditor/logs_controller.rb +3 -1
  34. data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
  35. data/app/services/mbeditor/editor_state_service.rb +36 -26
  36. data/app/services/mbeditor/exclusion_matcher.rb +12 -10
  37. data/app/services/mbeditor/file_operation_service.rb +38 -3
  38. data/app/services/mbeditor/file_tree_service.rb +30 -2
  39. data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
  40. data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
  41. data/app/services/mbeditor/git_diff_service.rb +5 -1
  42. data/app/services/mbeditor/git_info_service.rb +20 -8
  43. data/app/services/mbeditor/git_line_diff_service.rb +6 -2
  44. data/app/services/mbeditor/git_service.rb +65 -15
  45. data/app/services/mbeditor/js_definition_service.rb +3 -1
  46. data/app/services/mbeditor/js_globals_service.rb +7 -4
  47. data/app/services/mbeditor/js_members_service.rb +3 -2
  48. data/app/services/mbeditor/js_program_service.rb +15 -5
  49. data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
  50. data/app/services/mbeditor/process_runner.rb +42 -12
  51. data/app/services/mbeditor/ri_definition_service.rb +8 -1
  52. data/app/services/mbeditor/route_service.rb +45 -8
  53. data/app/services/mbeditor/rubocop_run_service.rb +86 -0
  54. data/app/services/mbeditor/ruby_definition_service.rb +23 -4
  55. data/app/services/mbeditor/schema_service.rb +65 -64
  56. data/app/services/mbeditor/search_replace_service.rb +75 -10
  57. data/app/services/mbeditor/test_runner_service.rb +98 -10
  58. data/lib/mbeditor/cable_log_filter.rb +8 -2
  59. data/lib/mbeditor/configuration.rb +12 -1
  60. data/lib/mbeditor/editor_bootstrap.rb +18 -12
  61. data/lib/mbeditor/engine.rb +22 -3
  62. data/lib/mbeditor/exception_log.rb +2 -2
  63. data/lib/mbeditor/pending_migrations.rb +33 -0
  64. data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
  65. data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
  66. data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
  67. data/lib/mbeditor/route_map.rb +2 -0
  68. data/lib/mbeditor/ruby_lsp_client.rb +71 -12
  69. data/lib/mbeditor/version.rb +1 -1
  70. metadata +7 -2
@@ -418,6 +418,8 @@ var TabManager = (function () {
418
418
  }
419
419
 
420
420
  function closeTab(paneId, path) {
421
+ // A throttled content write must not outlive the tab it belongs to.
422
+ flushContent();
421
423
  if (typeof HistoryService !== 'undefined') {
422
424
  HistoryService.flushForPath(path);
423
425
  }
@@ -539,23 +541,73 @@ var TabManager = (function () {
539
541
  EditorStore.setState({ focusedPaneId: paneId, activeTabId: newActive });
540
542
  }
541
543
 
544
+ // Typing calls markDirty/markClean per keystroke, and every store write
545
+ // re-renders the whole app (MbeditorApp subscribes to the entire store). Two
546
+ // rules keep that off the keystroke path:
547
+ //
548
+ // * the dirty flag is only written on a clean<->dirty transition;
549
+ // * `content` is written on a 250 ms trailing edge, so the store sees at
550
+ // most four content updates a second.
551
+ //
552
+ // FLUSH CONTRACT: anything that reads `tab.content` immediately after an edit
553
+ // — save, save-all, format, format-all — must call TabManager.flushContent()
554
+ // first and then re-read the tab from the store, or it works on text up to
555
+ // 250 ms old. Everything else (lint, EOL indicator, markdown preview, the
556
+ // external-change poll) tolerates the lag by design.
557
+ var CONTENT_WRITE_MS = 250;
558
+ var _pendingContent = {}; // "<paneId> <tabId>" -> { paneId, path, content, markdown }
559
+ var _contentTimer = null;
560
+
561
+ function _queueContent(paneId, path, content, updates, markdown) {
562
+ var key = paneId + '' + path;
563
+ if (updates) {
564
+ // A state transition has to land now, and it takes the content with it.
565
+ delete _pendingContent[key];
566
+ updates.content = content;
567
+ _updateTab(paneId, path, updates);
568
+ if (markdown) _syncMarkdownPreviewContent(path, content);
569
+ return;
570
+ }
571
+ _pendingContent[key] = { paneId: paneId, path: path, content: content, markdown: markdown };
572
+ if (_contentTimer === null) {
573
+ _contentTimer = setTimeout(flushContent, CONTENT_WRITE_MS);
574
+ }
575
+ }
576
+
577
+ function flushContent() {
578
+ if (_contentTimer !== null) {
579
+ clearTimeout(_contentTimer);
580
+ _contentTimer = null;
581
+ }
582
+ var pending = _pendingContent;
583
+ _pendingContent = {};
584
+ Object.keys(pending).forEach(function (k) {
585
+ var p = pending[k];
586
+ _updateTab(p.paneId, p.path, { content: p.content });
587
+ if (p.markdown) _syncMarkdownPreviewContent(p.path, p.content);
588
+ });
589
+ }
590
+
542
591
  function markDirty(paneId, path, content) {
543
592
  // Auto-harden any soft-open tab on first edit.
544
593
  var st = EditorStore.getState();
545
594
  var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
546
595
  var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
547
- var updates = { content: content, dirty: true };
596
+ var updates = null;
597
+ if (!existingTab || !existingTab.dirty) updates = { dirty: true };
548
598
  if (existingTab && existingTab.isSoftOpen) {
599
+ updates = updates || {};
549
600
  updates.isSoftOpen = false;
550
601
  }
551
- _updateTab(paneId, path, updates);
552
- if (_isMarkdownPath(path)) {
553
- _syncMarkdownPreviewContent(path, content);
554
- }
602
+ _queueContent(paneId, path, content, updates, _isMarkdownPath(path));
555
603
  }
556
604
 
557
605
  function markClean(paneId, path, content) {
558
- _updateTab(paneId, path, { content: content, dirty: false });
606
+ var st = EditorStore.getState();
607
+ var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
608
+ var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
609
+ var updates = (existingTab && !existingTab.dirty) ? null : { dirty: false };
610
+ _queueContent(paneId, path, content, updates, _isMarkdownPath(path));
559
611
  }
560
612
 
561
613
  function hardenTab(paneId, path) {
@@ -694,6 +746,7 @@ var TabManager = (function () {
694
746
  focusPane: focusPane,
695
747
  markDirty: markDirty,
696
748
  markClean: markClean,
749
+ flushContent: flushContent,
697
750
  hardenTab: hardenTab,
698
751
  saveTabViewState: saveTabViewState,
699
752
  reorderTabInPane: reorderTabInPane,
@@ -8,8 +8,14 @@
8
8
  // otherwise prevent reconnection.
9
9
  var WebSocketService = (function () {
10
10
  var _consumer = null;
11
+ // True only when _consumer is one we created ourselves; a consumer borrowed
12
+ // from the host app (window.App.cable) is not ours to disconnect.
13
+ var _ownsConsumer = false;
11
14
  var _subscription = null;
12
15
  var _connected = false;
16
+ // Distinguishes the first successful handshake from a reconnect, which is the
17
+ // point at which collaboration rooms have to be re-established.
18
+ var _everConnected = false;
13
19
  // Why collaboration is or is not working, so the editor can say so instead of
14
20
  // failing silently. A rejected subscription used to just schedule a reconnect
15
21
  // and tell nobody, which made a blocked cable indistinguishable from an empty
@@ -62,8 +68,10 @@ var WebSocketService = (function () {
62
68
  // Reuse an existing consumer the host app may have already created (App.cable
63
69
  // is the Rails default). Fall back to creating our own.
64
70
  if (typeof window.App !== 'undefined' && window.App.cable) {
71
+ _ownsConsumer = false;
65
72
  return window.App.cable;
66
73
  }
74
+ _ownsConsumer = true;
67
75
  return window.ActionCable.createConsumer(_cableUrl());
68
76
  }
69
77
 
@@ -72,10 +80,14 @@ var WebSocketService = (function () {
72
80
  try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
73
81
  _subscription = null;
74
82
  }
75
- if (_consumer && typeof _consumer.disconnect === 'function') {
83
+ // Only ever disconnect a consumer we created. When the host app already had
84
+ // one (window.App.cable) it is shared: disconnecting it on any mbeditor
85
+ // cable blip took down the host's own channels with it.
86
+ if (_ownsConsumer && _consumer && typeof _consumer.disconnect === 'function') {
76
87
  try { _consumer.disconnect(); } catch (e) { /* ignore */ }
77
88
  }
78
89
  _consumer = null;
90
+ _ownsConsumer = false;
79
91
  _connected = false;
80
92
  }
81
93
 
@@ -139,11 +151,21 @@ var WebSocketService = (function () {
139
151
  { channel: 'Mbeditor::EditorChannel' },
140
152
  {
141
153
  connected: function () {
154
+ var reconnected = _everConnected;
155
+ _everConnected = true;
142
156
  _connected = true;
143
157
  _status = 'connected';
144
158
  // Back to fast retries for the next blip.
145
159
  _reconnectAttempts = 0;
146
160
  if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
161
+ // The drop took the old consumer down and every CollaborationChannel
162
+ // subscription made on it with it. Nothing else notices — cable
163
+ // availability is about support, not liveness — so the rooms would
164
+ // stay silently dead until each file was closed and reopened.
165
+ if (reconnected && typeof CollaborationService !== 'undefined' &&
166
+ typeof CollaborationService.rejoinRooms === 'function') {
167
+ try { CollaborationService.rejoinRooms(); } catch (e) { /* ignore */ }
168
+ }
147
169
  },
148
170
  disconnected: function () {
149
171
  _status = _status === 'rejected' ? 'rejected' : 'dropped';
@@ -178,12 +200,71 @@ var WebSocketService = (function () {
178
200
  }
179
201
  }
180
202
 
181
- function _emitFilesChanged(data) {
182
- _filesChangedCallbacks.forEach(function (fn) {
183
- try { fn(data); } catch (e) { /* ignore */ }
203
+ // ── files_changed coalescing ───────────────────────────────────────────────
204
+ //
205
+ // The server broadcasts once per written file, and each subscriber turns a
206
+ // broadcast into HTTP work: the app refreshes the tree and git status, the
207
+ // Monaco plugin refreshes the TypeScript program, every open editor pane
208
+ // re-runs git line-diff. Saving several files in a row — paste, save, next
209
+ // file — therefore multiplied one write into a fan-out per save, and past
210
+ // the point where the dev server could keep up the queue simply grew: saves
211
+ // got slower and slower until requests hit the axios timeout.
212
+ //
213
+ // MbeditorApp already debounced its own handler for exactly this reason, but
214
+ // a debounce there cannot help the other subscribers. Coalescing here fixes
215
+ // every subscriber at once, including any added later.
216
+ var FILES_CHANGED_COALESCE_MS = 150;
217
+ var _filesChangedTimer = null;
218
+ var _filesChangedAcc = null;
219
+
220
+ // Merge a broadcast into the accumulator. Pure so it can be tested directly;
221
+ // the two invariants are easy to get wrong and both have teeth:
222
+ //
223
+ // - A broadcast with no paths means "something changed, re-check
224
+ // everything". It must not be narrowed by paths that happen to land in
225
+ // the same window, or the re-check silently covers only those files.
226
+ // - `structural` is a union: one structural broadcast in the burst makes
227
+ // the whole flush structural, because a tree walk that gets skipped
228
+ // leaves a created or deleted file missing from the explorer.
229
+ function _mergeFilesChanged(acc, data) {
230
+ var next = acc || { paths: [], pathless: false, structural: false };
231
+ if (data && data.paths) next.paths = next.paths.concat(data.paths);
232
+ else next.pathless = true;
233
+ // Absent `structural` means an older server — the conservative read is true.
234
+ if (!data || data.structural !== false) next.structural = true;
235
+ return next;
236
+ }
237
+
238
+ function _coalescedPayload(acc) {
239
+ var payload = { type: 'files_changed', structural: acc.structural };
240
+ if (!acc.pathless && acc.paths.length) {
241
+ var seen = {};
242
+ payload.paths = acc.paths.filter(function (p) {
243
+ if (seen['$' + p]) return false;
244
+ seen['$' + p] = true;
245
+ return true;
246
+ });
247
+ }
248
+ return payload;
249
+ }
250
+
251
+ function _flushFilesChanged() {
252
+ _filesChangedTimer = null;
253
+ var acc = _filesChangedAcc;
254
+ _filesChangedAcc = null;
255
+ if (!acc) return;
256
+ var payload = _coalescedPayload(acc);
257
+ _filesChangedCallbacks.slice().forEach(function (fn) {
258
+ try { fn(payload); } catch (e) { /* ignore */ }
184
259
  });
185
260
  }
186
261
 
262
+ function _emitFilesChanged(data) {
263
+ _filesChangedAcc = _mergeFilesChanged(_filesChangedAcc, data);
264
+ if (_filesChangedTimer) return;
265
+ _filesChangedTimer = setTimeout(_flushFilesChanged, FILES_CHANGED_COALESCE_MS);
266
+ }
267
+
187
268
  function _emitFileSaved(data) {
188
269
  _fileSavedCallbacks.forEach(function (fn) {
189
270
  try { fn(data); } catch (e) { /* ignore */ }
@@ -342,6 +423,9 @@ var WebSocketService = (function () {
342
423
  subscribeCollaboration: subscribeCollaboration,
343
424
  perform: perform,
344
425
  onFilesChanged: onFilesChanged,
426
+ // Exposed for tests — the burst-merge invariants above are worth pinning.
427
+ _mergeFilesChanged: _mergeFilesChanged,
428
+ _coalescedPayload: _coalescedPayload,
345
429
  offFilesChanged: offFilesChanged,
346
430
  onFileSaved: onFileSaved,
347
431
  offFileSaved: offFileSaved,