mbeditor 0.13.0 → 0.14.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -1
  3. data/app/assets/javascripts/mbeditor/application.js +3 -1
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +440 -334
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  17. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  18. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
  19. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  20. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
  21. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
  22. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  23. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  24. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  25. data/app/assets/javascripts/mbeditor/editor_plugins.js +694 -306
  26. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  27. data/app/assets/javascripts/mbeditor/file_service.js +46 -57
  28. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  29. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  30. data/app/assets/javascripts/mbeditor/search_service.js +29 -2
  31. data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
  32. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  33. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  34. data/app/assets/stylesheets/mbeditor/editor.css +762 -297
  35. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  36. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  37. data/app/channels/mbeditor/collaboration_channel.rb +25 -6
  38. data/app/controllers/mbeditor/application_controller.rb +26 -3
  39. data/app/controllers/mbeditor/editors_controller.rb +125 -465
  40. data/app/services/mbeditor/archive_service.rb +137 -0
  41. data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
  42. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  43. data/app/services/mbeditor/editor_state_service.rb +14 -51
  44. data/app/services/mbeditor/file_history_service.rb +222 -0
  45. data/app/services/mbeditor/git_info_service.rb +6 -0
  46. data/app/services/mbeditor/js_globals_service.rb +12 -1
  47. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  48. data/app/services/mbeditor/lint_service.rb +137 -0
  49. data/app/services/mbeditor/locked_json_file.rb +67 -0
  50. data/app/services/mbeditor/process_runner.rb +32 -0
  51. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  52. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  53. data/app/services/mbeditor/schema_service.rb +8 -2
  54. data/app/services/mbeditor/search_replace_service.rb +19 -2
  55. data/app/services/mbeditor/test_runner_service.rb +3 -56
  56. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  57. data/lib/mbeditor/audit_log.rb +203 -0
  58. data/lib/mbeditor/configuration.rb +6 -9
  59. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  60. data/lib/mbeditor/route_map.rb +4 -1
  61. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  62. data/lib/mbeditor/version.rb +1 -1
  63. data/lib/mbeditor.rb +1 -0
  64. data/lib/tasks/mbeditor.rake +23 -0
  65. metadata +14 -3
  66. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -145,10 +145,12 @@ var TabManager = (function () {
145
145
  });
146
146
  }
147
147
 
148
- // Single slot holding "where the cursor was before the last jump".
149
- // PageUp/PageDown swap the cursor between this slot and its current spot.
150
- // ponytail: two-position cycle, upgrade to a real back/forward stack if asked
151
- var _jumpOrigin = null;
148
+ // Back/forward navigation history: every real jump or file change pushes
149
+ // an entry; PageUp/PageDown walk it like browser back/forward.
150
+ var _navStack = [];
151
+ var _navIndex = -1;
152
+ var _navigating = false;
153
+ var NAV_STACK_MAX = 50;
152
154
 
153
155
  function _snapshotPosition() {
154
156
  var editor = window.__mbeditorActiveEditor;
@@ -161,16 +163,40 @@ var TabManager = (function () {
161
163
  return { path: tab.path, name: tab.name, line: pos.lineNumber, col: pos.column };
162
164
  }
163
165
 
164
- function toggleJumpOrigin() {
165
- var target = _jumpOrigin;
166
- if (!target) return;
167
- // openTab re-snapshots the current position into _jumpOrigin below,
168
- // which is exactly the swap we want.
169
- openTab(target.path, target.name, target.line, null, false, target.col);
170
- }
171
-
172
- function openTab(path, name, line, forcePaneId, isSoftOpen, col) {
173
- if (line) _jumpOrigin = _snapshotPosition() || _jumpOrigin;
166
+ // Truncates any forward entries, then pushes. Skips a push that would
167
+ // duplicate the current top entry's path+line, and is a no-op entirely
168
+ // while a navigateBack/navigateForward-driven openTab is replaying.
169
+ function _pushNavEntry(entry) {
170
+ if (_navigating || !entry || !entry.path) return;
171
+ if (_navIndex > -1) _navStack = _navStack.slice(0, _navIndex + 1);
172
+ var top = _navStack[_navStack.length - 1];
173
+ if (top && top.path === entry.path && top.line === entry.line) return;
174
+ _navStack.push(entry);
175
+ if (_navStack.length > NAV_STACK_MAX) _navStack.shift();
176
+ _navIndex = _navStack.length - 1;
177
+ }
178
+
179
+ // Refreshing the entry we are leaving from the live cursor is what makes
180
+ // going back land where you actually were, not where the entry was created.
181
+ function _navigate(step) {
182
+ var next = _navIndex + step;
183
+ if (_navIndex < 0 || next < 0 || next >= _navStack.length) return;
184
+ var live = _snapshotPosition();
185
+ if (live) _navStack[_navIndex] = live;
186
+ _navIndex = next;
187
+ var target = _navStack[next];
188
+ _navigating = true;
189
+ try { openTab(target.path, target.name, target.line, null, false, target.col); }
190
+ finally { _navigating = false; }
191
+ }
192
+
193
+ function navigateBack() { _navigate(-1); }
194
+ function navigateForward() { _navigate(1); }
195
+
196
+ // col/endCol are the 1-based bounds of the thing being jumped to. Pass both
197
+ // and the editor selects it, leaving the cursor at its end; pass col alone
198
+ // and it just parks the cursor there.
199
+ function openTab(path, name, line, forcePaneId, isSoftOpen, col, endCol) {
174
200
  var state = EditorStore.getState();
175
201
  var paneId = forcePaneId || state.focusedPaneId;
176
202
  var pane = state.panes.find(function(p) { return p.id === paneId; });
@@ -186,13 +212,14 @@ var TabManager = (function () {
186
212
 
187
213
  if (!pane) return;
188
214
 
215
+ _pushNavEntry({ path: path, name: name, line: line || null, col: col || null });
189
216
  _recordRecentFile(path, name);
190
217
 
191
218
  var existing = pane.tabs.find(function(t) { return t.path === path; });
192
219
 
193
220
  if (existing) {
194
- if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null });
195
- switchTab(paneId, path);
221
+ if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null, gotoEndCol: endCol || null });
222
+ _switchTab(paneId, path);
196
223
  if (_isMarkdownPath(path)) {
197
224
  _ensureMarkdownPreview(paneId, path, existing.name || name, existing.content || "");
198
225
  }
@@ -217,7 +244,7 @@ var TabManager = (function () {
217
244
  isSoftOpen: isSoftOpen ? true : false,
218
245
  loading: true
219
246
  };
220
- if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; }
247
+ if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; newTab.gotoEndCol = endCol || null; }
221
248
 
222
249
  var newPanes = state.panes.map(function(p) {
223
250
  if (p.id === paneId) {
@@ -227,6 +254,7 @@ var TabManager = (function () {
227
254
  });
228
255
 
229
256
  EditorStore.setState({ panes: newPanes, focusedPaneId: paneId, activeTabId: path });
257
+ _promoteMru(paneId, path);
230
258
 
231
259
  // Virtual paths (diff://, combined-diff://) should never hit the file endpoint
232
260
  if (path.indexOf('diff://') === 0 || path.indexOf('combined-diff://') === 0) {
@@ -234,6 +262,7 @@ var TabManager = (function () {
234
262
  }
235
263
 
236
264
  // Use a prefetched result if available (hover-prefetch hit), otherwise fetch normally.
265
+ var startedAt = Date.now();
237
266
  var prefetchPromise = FileService.getPrefetched(path);
238
267
  var filePromise = prefetchPromise || FileService.getFile(path, { allowMissing: true });
239
268
 
@@ -246,6 +275,11 @@ var TabManager = (function () {
246
275
  if (!data) { closeTab(paneId, path); return; }
247
276
  var loadedContent = typeof data.content === 'string' ? data.content : "";
248
277
  var fileNotFound = data && data.missing === true;
278
+ var audit = window.MbeditorAudit;
279
+ if (audit) {
280
+ audit.rec(audit.EV.OPEN, audit.code('ext', String(path).split('.').pop().toLowerCase()),
281
+ loadedContent.length, Date.now() - startedAt);
282
+ }
249
283
  _updateTab(paneId, path, {
250
284
  content: loadedContent,
251
285
  cleanContent: loadedContent,
@@ -260,6 +294,8 @@ var TabManager = (function () {
260
294
  _syncMarkdownPreviewContent(path, typeof data.content === 'string' ? data.content : "");
261
295
  }
262
296
  }).catch(function(err) {
297
+ var audit = window.MbeditorAudit;
298
+ if (audit) audit.rec(audit.EV.ERR, audit.EV.OPEN);
263
299
  if (path.startsWith('diff://')) return; // diff tabs handle their own loading
264
300
  if (err.response && err.response.status === 413) {
265
301
  FileService.getFileChunk(path, 0, 500).then(function(data) {
@@ -423,18 +459,34 @@ var TabManager = (function () {
423
459
  if (typeof HistoryService !== 'undefined') {
424
460
  HistoryService.flushForPath(path);
425
461
  }
462
+ _removeMru(paneId, path);
426
463
  var state = EditorStore.getState();
464
+ // Closing a markdown source also closes its preview tab, wherever it lives.
465
+ // Guard against recursion: a preview tab has no preview of its own.
466
+ var closingPane = state.panes.find(function(p) { return p.id === paneId; });
467
+ var closingTab = closingPane && closingPane.tabs.find(function(t) { return t.id === path; });
468
+ var closePreviewsFor = (closingTab && !closingTab.isPreview) ? path : null;
469
+
427
470
  var newPanes = state.panes.map(function(pane) {
428
- if (pane.id === paneId) {
429
- // Match by t.id so diff tabs (id = 'diff://...') are closed correctly
430
- var newTabs = pane.tabs.filter(function(t) { return t.id !== path; });
431
- var newActive = pane.activeTabId;
432
- if (pane.activeTabId === path) {
433
- newActive = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
471
+ // Match by t.id so diff tabs (id = 'diff://...') are closed correctly
472
+ var newTabs = pane.tabs.filter(function(t) {
473
+ if (pane.id === paneId && t.id === path) return false;
474
+ if (closePreviewsFor && t.isPreview && t.previewFor === closePreviewsFor) return false;
475
+ return true;
476
+ });
477
+ if (newTabs.length === pane.tabs.length) return pane;
478
+
479
+ var newActive = pane.activeTabId;
480
+ if (!newTabs.some(function(t) { return t.id === newActive; })) {
481
+ // Reactivate the tab the user was actually last in, not just the rightmost one.
482
+ var mruList = _mru[pane.id] || [];
483
+ var mruPick = null;
484
+ for (var i = 0; i < mruList.length; i++) {
485
+ if (newTabs.some(function(t) { return t.id === mruList[i]; })) { mruPick = mruList[i]; break; }
434
486
  }
435
- return Object.assign({}, pane, { tabs: newTabs, activeTabId: newActive });
487
+ newActive = mruPick || (newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null);
436
488
  }
437
- return pane;
489
+ return Object.assign({}, pane, { tabs: newTabs, activeTabId: newActive });
438
490
  });
439
491
 
440
492
  var nextFocusedPaneId = state.focusedPaneId;
@@ -494,7 +546,7 @@ var TabManager = (function () {
494
546
  if (!pane || pane.tabs.length === 0) return;
495
547
 
496
548
  pane.tabs.slice().forEach(function(tab) {
497
- closeTab(paneId, tab.path);
549
+ closeTab(paneId, tab.id);
498
550
  });
499
551
  }
500
552
 
@@ -525,13 +577,43 @@ var TabManager = (function () {
525
577
  });
526
578
  }
527
579
 
528
- function switchTab(paneId, path) {
580
+ // Most-recently-used tab ids per pane, most-recent first. Lets closeTab
581
+ // reactivate the tab the user was actually last in, not just the rightmost one.
582
+ var _mru = {};
583
+ var MRU_MAX = 20;
584
+
585
+ function _promoteMru(paneId, tabId) {
586
+ if (!tabId) return;
587
+ var list = _mru[paneId] || (_mru[paneId] = []);
588
+ var idx = list.indexOf(tabId);
589
+ if (idx !== -1) list.splice(idx, 1);
590
+ list.unshift(tabId);
591
+ if (list.length > MRU_MAX) list.length = MRU_MAX;
592
+ }
593
+
594
+ function _removeMru(paneId, tabId) {
595
+ var list = _mru[paneId];
596
+ if (!list) return;
597
+ var idx = list.indexOf(tabId);
598
+ if (idx !== -1) list.splice(idx, 1);
599
+ }
600
+
601
+ function _switchTab(paneId, path) {
529
602
  var state = EditorStore.getState();
530
603
  var newPanes = state.panes.map(function(p) {
531
604
  if (p.id === paneId) return Object.assign({}, p, { activeTabId: path });
532
605
  return p;
533
606
  });
534
607
  EditorStore.setState({ panes: newPanes, focusedPaneId: paneId, activeTabId: path });
608
+ _promoteMru(paneId, path);
609
+ }
610
+
611
+ function switchTab(paneId, path) {
612
+ _switchTab(paneId, path);
613
+ var state = EditorStore.getState();
614
+ var pane = state.panes.find(function(p) { return p.id === paneId; });
615
+ var tab = pane && pane.tabs.find(function(t) { return t.id === path; });
616
+ _pushNavEntry({ path: path, name: tab ? tab.name : null, line: null, col: null });
535
617
  }
536
618
 
537
619
  function focusPane(paneId) {
@@ -561,12 +643,12 @@ var TabManager = (function () {
561
643
  function _queueContent(paneId, path, content, updates, markdown) {
562
644
  var key = paneId + '' + path;
563
645
  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;
646
+ // A clean<->dirty transition must land now, but the full buffer is still
647
+ // materialized only on the trailing edge below. Callers pass a provider
648
+ // (a function) from the per-keystroke content listener so getValue() runs
649
+ // once per flush instead of once per keypress; everything else passes the
650
+ // string it already holds.
567
651
  _updateTab(paneId, path, updates);
568
- if (markdown) _syncMarkdownPreviewContent(path, content);
569
- return;
570
652
  }
571
653
  _pendingContent[key] = { paneId: paneId, path: path, content: content, markdown: markdown };
572
654
  if (_contentTimer === null) {
@@ -574,6 +656,13 @@ var TabManager = (function () {
574
656
  }
575
657
  }
576
658
 
659
+ // A pending content slot holds either a string or a provider invoked at flush
660
+ // time. A provider whose model has gone away returns nothing and is skipped.
661
+ function _resolveContent(content) {
662
+ if (typeof content !== 'function') return content;
663
+ try { return content(); } catch (e) { return null; }
664
+ }
665
+
577
666
  function flushContent() {
578
667
  if (_contentTimer !== null) {
579
668
  clearTimeout(_contentTimer);
@@ -583,8 +672,10 @@ var TabManager = (function () {
583
672
  _pendingContent = {};
584
673
  Object.keys(pending).forEach(function (k) {
585
674
  var p = pending[k];
586
- _updateTab(p.paneId, p.path, { content: p.content });
587
- if (p.markdown) _syncMarkdownPreviewContent(p.path, p.content);
675
+ var content = _resolveContent(p.content);
676
+ if (typeof content !== 'string') return;
677
+ _updateTab(p.paneId, p.path, { content: content });
678
+ if (p.markdown) _syncMarkdownPreviewContent(p.path, content);
588
679
  });
589
680
  }
590
681
 
@@ -614,24 +705,13 @@ var TabManager = (function () {
614
705
  _updateTab(paneId, path, { isSoftOpen: false });
615
706
  }
616
707
 
617
- function saveTabViewState(paneIdOrPath, pathOrViewState, maybeViewState) {
618
- var paneId = paneIdOrPath;
619
- var path = pathOrViewState;
620
- var viewState = maybeViewState;
621
-
622
- // Backward-compatible signature: saveTabViewState(path, viewState)
623
- if (typeof maybeViewState === 'undefined') {
624
- var state = EditorStore.getState();
625
- path = paneIdOrPath;
626
- viewState = pathOrViewState;
627
- var containingPane = state.panes.find(function(p) {
628
- return p.tabs.some(function(t) { return t.path === path; });
629
- });
630
- if (!containingPane) return;
631
- paneId = containingPane.id;
632
- }
633
-
634
- _updateTab(paneId, path, { viewState: viewState });
708
+ function saveTabViewState(path, viewState) {
709
+ var state = EditorStore.getState();
710
+ var containingPane = state.panes.find(function(p) {
711
+ return p.tabs.some(function(t) { return t.path === path; });
712
+ });
713
+ if (!containingPane) return;
714
+ _updateTab(containingPane.id, path, { viewState: viewState });
635
715
  }
636
716
 
637
717
  function reorderTabInPane(paneId, tabId, insertBeforeTabId) {
@@ -699,7 +779,7 @@ var TabManager = (function () {
699
779
  }
700
780
 
701
781
  function clearGotoLine(paneId, path) {
702
- _updateTab(paneId, path, { gotoLine: null, gotoCol: null });
782
+ _updateTab(paneId, path, { gotoLine: null, gotoCol: null, gotoEndCol: null });
703
783
  }
704
784
 
705
785
  // VS Code-style scratch buffer: a tab with no file behind it. Nothing is
@@ -752,7 +832,8 @@ var TabManager = (function () {
752
832
  reorderTabInPane: reorderTabInPane,
753
833
  moveTabToPane: moveTabToPane,
754
834
  clearGotoLine: clearGotoLine,
755
- toggleJumpOrigin: toggleJumpOrigin,
835
+ navigateBack: navigateBack,
836
+ navigateForward: navigateForward,
756
837
  closeAllTabsInPane: closeAllTabsInPane,
757
838
  closeOtherTabsInPane: closeOtherTabsInPane,
758
839
  closeSavedTabsInPane: closeSavedTabsInPane,
@@ -75,6 +75,17 @@ var WebSocketService = (function () {
75
75
  return window.ActionCable.createConsumer(_cableUrl());
76
76
  }
77
77
 
78
+ // Single consumer for both the EditorChannel and every CollaborationChannel.
79
+ // subscribeCollaboration used to mint its own consumer whenever _consumer was
80
+ // null (the state after a drop), and _attemptConnect then created a second one
81
+ // — the first socket was never disconnected, so every outage during which a
82
+ // file was opened leaked a WebSocket. Whoever asks first creates it; everyone
83
+ // else reuses it.
84
+ function _ensureConsumer() {
85
+ if (!_consumer) _consumer = _getConsumer();
86
+ return _consumer;
87
+ }
88
+
78
89
  function _cleanupConsumer() {
79
90
  if (_subscription) {
80
91
  try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
@@ -146,7 +157,7 @@ var WebSocketService = (function () {
146
157
  if (_status !== 'rejected') _status = 'connecting';
147
158
 
148
159
  try {
149
- _consumer = _getConsumer();
160
+ _consumer = _ensureConsumer();
150
161
  _subscription = _consumer.subscriptions.create(
151
162
  { channel: 'Mbeditor::EditorChannel' },
152
163
  {
@@ -344,10 +355,7 @@ var WebSocketService = (function () {
344
355
  if (!isCableAvailable()) return null;
345
356
  handlers = handlers || {};
346
357
  try {
347
- if (!_consumer) {
348
- _consumer = _getConsumer();
349
- }
350
- return _consumer.subscriptions.create(
358
+ return _ensureConsumer().subscriptions.create(
351
359
  { channel: 'Mbeditor::CollaborationChannel', path: path },
352
360
  {
353
361
  connected: function () { if (handlers.connected) handlers.connected(); },
@@ -2,6 +2,7 @@
2
2
  *= require pico.classless
3
3
  *= require mbeditor/themes
4
4
  *= require mbeditor/editor
5
+ *= require mbeditor/glass
5
6
  */
6
7
 
7
8
  /* ── Git & Code Review Styles ───────────────────────────────────────── */
@@ -641,7 +642,7 @@
641
642
  }
642
643
  .redmine-badge {
643
644
  background-color: var(--ide-accent);
644
- color: var(--ide-accent-text);
645
+ color: var(--ide-on-accent);
645
646
  font-size: 10px;
646
647
  padding: 2px 6px;
647
648
  border-radius: 10px;
@@ -858,6 +859,10 @@
858
859
  .search-loading-spinner {
859
860
  animation-duration: 0.7s !important;
860
861
  }
862
+
863
+ .statusbar-offline {
864
+ animation: none;
865
+ }
861
866
  }
862
867
 
863
868
  .cdiff-ctx { color: var(--ide-text-muted); }