mbeditor 0.13.1 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/app/assets/javascripts/mbeditor/application.js +3 -0
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
  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 +429 -247
  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/LogPanel.js +3 -44
  17. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
  18. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  19. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
  20. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
  21. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  22. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  23. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  24. data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
  25. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  26. data/app/assets/javascripts/mbeditor/file_service.js +38 -39
  27. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  28. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  29. data/app/assets/javascripts/mbeditor/search_service.js +9 -0
  30. data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
  31. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  32. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  33. data/app/assets/stylesheets/mbeditor/editor.css +642 -248
  34. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  35. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  36. data/app/channels/mbeditor/collaboration_channel.rb +17 -4
  37. data/app/controllers/mbeditor/application_controller.rb +26 -3
  38. data/app/controllers/mbeditor/editors_controller.rb +117 -439
  39. data/app/services/mbeditor/archive_service.rb +137 -0
  40. data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
  41. data/app/services/mbeditor/editor_state_service.rb +14 -51
  42. data/app/services/mbeditor/file_history_service.rb +222 -0
  43. data/app/services/mbeditor/git_info_service.rb +6 -0
  44. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  45. data/app/services/mbeditor/lint_service.rb +137 -0
  46. data/app/services/mbeditor/locked_json_file.rb +67 -0
  47. data/app/services/mbeditor/process_runner.rb +32 -0
  48. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  49. data/app/services/mbeditor/search_replace_service.rb +8 -0
  50. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  51. data/lib/mbeditor/audit_log.rb +203 -0
  52. data/lib/mbeditor/configuration.rb +6 -1
  53. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  54. data/lib/mbeditor/route_map.rb +4 -0
  55. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  56. data/lib/mbeditor/version.rb +1 -1
  57. data/lib/mbeditor.rb +1 -0
  58. metadata +12 -2
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
4
3
 
5
4
  var _React = React;
6
5
  var useState = _React.useState;
@@ -39,6 +38,11 @@ var EditorPanel = function EditorPanel(_ref) {
39
38
  var monacoRef = useRef(null);
40
39
  var latestContentRef = useRef('');
41
40
  var lastAppliedExternalVersionRef = useRef(0);
41
+ // Set by the editor-creation effect when the tab mounts before its file content
42
+ // has arrived. The external-content effect calls it once the load lands, so
43
+ // persistent-undo tracking starts with the loaded file as its base instead of
44
+ // recording the load itself as an undo op.
45
+ var pendingHistorySetupRef = useRef(null);
42
46
  var conflictDecorationsRef = React.useRef([]);
43
47
  var conflictBlocksRef = React.useRef([]);
44
48
  var aviBaseRef = useRef(0);
@@ -67,24 +71,20 @@ var EditorPanel = function EditorPanel(_ref) {
67
71
  var currentConflictIndexRef = React.useRef(0);
68
72
 
69
73
  var _useState = useState('');
70
- var _useState2 = _slicedToArray(_useState, 2);
71
- var markup = _useState2[0];
72
- var setMarkup = _useState2[1];
74
+ var markup = _useState[0];
75
+ var setMarkup = _useState[1];
73
76
 
74
77
  var _useState3 = useState(false);
75
- var _useState4 = _slicedToArray(_useState3, 2);
76
- var isBlameVisible = _useState4[0];
77
- var setIsBlameVisible = _useState4[1];
78
+ var isBlameVisible = _useState3[0];
79
+ var setIsBlameVisible = _useState3[1];
78
80
 
79
81
  var _useState5 = useState(null);
80
- var _useState6 = _slicedToArray(_useState5, 2);
81
- var blameData = _useState6[0];
82
- var setBlameData = _useState6[1];
82
+ var blameData = _useState5[0];
83
+ var setBlameData = _useState5[1];
83
84
 
84
85
  var _useState7 = useState(false);
85
- var _useState8 = _slicedToArray(_useState7, 2);
86
- var isBlameLoading = _useState8[0];
87
- var setIsBlameLoading = _useState8[1];
86
+ var isBlameLoading = _useState7[0];
87
+ var setIsBlameLoading = _useState7[1];
88
88
 
89
89
  var blameDecorationsRef = useRef([]);
90
90
  var gitLineDecorationsRef = useRef([]);
@@ -97,34 +97,28 @@ var EditorPanel = function EditorPanel(_ref) {
97
97
  var testZoneIdsRef = useRef([]);
98
98
 
99
99
  var _useState9 = useState(false);
100
- var _useState10 = _slicedToArray(_useState9, 2);
101
- var editorReady = _useState10[0];
102
- var setEditorReady = _useState10[1];
100
+ var editorReady = _useState9[0];
101
+ var setEditorReady = _useState9[1];
103
102
 
104
103
  var _useState11 = useState(false);
105
- var _useState12 = _slicedToArray(_useState11, 2);
106
- var methodsOpen = _useState12[0];
107
- var setMethodsOpen = _useState12[1];
104
+ var methodsOpen = _useState11[0];
105
+ var setMethodsOpen = _useState11[1];
108
106
 
109
107
  var _useState13 = useState([]);
110
- var _useState14 = _slicedToArray(_useState13, 2);
111
- var methodsList = _useState14[0];
112
- var setMethodsList = _useState14[1];
108
+ var methodsList = _useState13[0];
109
+ var setMethodsList = _useState13[1];
113
110
 
114
111
  var _useState15 = useState(false);
115
- var _useState16 = _slicedToArray(_useState15, 2);
116
- var methodsTruncated = _useState16[0];
117
- var setMethodsTruncated = _useState16[1];
112
+ var methodsTruncated = _useState15[0];
113
+ var setMethodsTruncated = _useState15[1];
118
114
 
119
115
  var _useState17 = useState(false);
120
- var _useState18 = _slicedToArray(_useState17, 2);
121
- var methodsUnavailable = _useState18[0];
122
- var setMethodsUnavailable = _useState18[1];
116
+ var methodsUnavailable = _useState17[0];
117
+ var setMethodsUnavailable = _useState17[1];
123
118
 
124
119
  var _useState19 = useState(null);
125
- var _useState20 = _slicedToArray(_useState19, 2);
126
- var methodsDropdownPos = _useState20[0];
127
- var setMethodsDropdownPos = _useState20[1];
120
+ var methodsDropdownPos = _useState19[0];
121
+ var setMethodsDropdownPos = _useState19[1];
128
122
 
129
123
  var methodsBtnRef = useRef(null);
130
124
  var methodsDropdownRef = useRef(null);
@@ -133,24 +127,20 @@ var EditorPanel = function EditorPanel(_ref) {
133
127
 
134
128
  // Local pagination state — initialized from tab props; updated on page navigation
135
129
  var _useState21 = useState(tab.startLine || 0);
136
- var _useState22 = _slicedToArray(_useState21, 2);
137
- var pageStartLine = _useState22[0];
138
- var setPageStartLine = _useState22[1];
130
+ var pageStartLine = _useState21[0];
131
+ var setPageStartLine = _useState21[1];
139
132
 
140
133
  var _useState23 = useState(tab.lineCount || 0);
141
- var _useState24 = _slicedToArray(_useState23, 2);
142
- var pageLineCount = _useState24[0];
143
- var setPageLineCount = _useState24[1];
134
+ var pageLineCount = _useState23[0];
135
+ var setPageLineCount = _useState23[1];
144
136
 
145
137
  var _useState25 = useState(tab.totalLines || 0);
146
- var _useState26 = _slicedToArray(_useState25, 2);
147
- var pageTotalLines = _useState26[0];
148
- var setPageTotalLines = _useState26[1];
138
+ var pageTotalLines = _useState25[0];
139
+ var setPageTotalLines = _useState25[1];
149
140
 
150
141
  var _useState27 = useState(tab.totalBytes || 0);
151
- var _useState28 = _slicedToArray(_useState27, 2);
152
- var pageTotalBytes = _useState28[0];
153
- var setPageTotalBytes = _useState28[1];
142
+ var pageTotalBytes = _useState27[0];
143
+ var setPageTotalBytes = _useState27[1];
154
144
 
155
145
  var onFormatRef = useRef(onFormat);
156
146
  onFormatRef.current = onFormat;
@@ -165,12 +155,7 @@ var EditorPanel = function EditorPanel(_ref) {
165
155
 
166
156
  var vimStatusRef = useRef(null);
167
157
  var vimModeObjRef = useRef(null);
168
-
169
- function humanSize(bytes) {
170
- if (bytes < 1024) return bytes + ' B';
171
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
172
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
173
- }
158
+ var previewScrollRef = useRef(null);
174
159
 
175
160
  var clearTestZones = function clearTestZones(editor) {
176
161
  if (!editor) return;
@@ -515,6 +500,10 @@ var EditorPanel = function EditorPanel(_ref) {
515
500
  language = 'javascript';
516
501
  } else if (/\.css\.erb$/.test(fileNameLower)) {
517
502
  language = 'css';
503
+ } else if (/\.scss\.erb$/.test(fileNameLower)) {
504
+ language = 'scss';
505
+ } else if (/\.less\.erb$/.test(fileNameLower)) {
506
+ language = 'less';
518
507
  } else if (/\.html\.erb$/.test(fileNameLower)) {
519
508
  language = 'erb';
520
509
  } else if (/\.html\.haml$/.test(fileNameLower)) {
@@ -540,8 +529,16 @@ var EditorPanel = function EditorPanel(_ref) {
540
529
  language = 'javascript';break;
541
530
  case 'ts':case 'tsx':
542
531
  language = 'typescript';break;
543
- case 'css':case 'scss':case 'sass':
532
+ // scss and less are their own Monaco languages, and reach nothing useful
533
+ // without these: scss went to css and got flagged line by line, less went
534
+ // nowhere and fell through to plaintext. '.sass' stays on css because
535
+ // Monaco has no sass language, and scss would reject it just as loudly.
536
+ case 'css':case 'sass':
544
537
  language = 'css';break;
538
+ case 'scss':
539
+ language = 'scss';break;
540
+ case 'less':
541
+ language = 'less';break;
545
542
  case 'html':
546
543
  language = 'html';break;
547
544
  case 'erb':
@@ -604,16 +601,26 @@ var EditorPanel = function EditorPanel(_ref) {
604
601
  // untitled://, mbeditor:// and friends have no file to persist history
605
602
  // against — tracking them just produces doomed /file_history requests.
606
603
  var _virtualPath = (tab.path || '').indexOf('://') >= 0;
607
-
608
- if (!_collabActive && !_virtualPath && typeof HistoryService !== 'undefined') {
604
+ var _historyEnabled = !_collabActive && !_virtualPath && typeof HistoryService !== 'undefined';
605
+ // A freshly opened tab mounts empty and receives its content asynchronously.
606
+ // Tracking must wait for that load (and start with the loaded file as its
607
+ // base) or the load is recorded as an insert-at-origin op — the root cause of
608
+ // both the doubled buffer (#92) and histories that grow by a file per open (#93).
609
+ var _contentLoaded = (tab.externalContentVersion || 0) > 0 ||
610
+ (_reusingModel && modelObj.getValue().length > 0);
611
+
612
+ if (_historyEnabled && _reusingModel && _contentLoaded) {
609
613
  var _histBranch = EditorStore.getState().gitBranch || '';
610
614
  if (_histBranch) {
611
- if (_reusingModel) {
612
- HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
613
- } else {
614
- HistoryService.beginTracking(_histBranch, tab.path, tab.content || '');
615
- }
615
+ HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
616
616
  }
617
+ } else if (_historyEnabled && _reusingModel) {
618
+ // A cached model that is still empty (the tab was switched away before the
619
+ // load landed). Defer like the new-model path so the load isn't recorded.
620
+ pendingHistorySetupRef.current = function () {
621
+ var _branch = EditorStore.getState().gitBranch || '';
622
+ if (_branch) HistoryService.resumeTracking(_branch, tab.path, modelObj.getValue());
623
+ };
617
624
  }
618
625
 
619
626
  // Sync latestContentRef from the actual model content so the onDidChangeContent
@@ -652,15 +659,20 @@ var EditorPanel = function EditorPanel(_ref) {
652
659
  quickSuggestions: editorPrefs.quickSuggestions !== false,
653
660
  wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
654
661
  acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on',
655
- linkedEditing: !!(editorPrefs.linkedEditing),
662
+ linkedEditing: true,
656
663
  fixedOverflowWidgets: true,
657
664
  hover: { above: false }
658
665
  });
659
666
 
660
667
  applyLargeFileTuning(editor, modelObj);
661
668
 
662
- if (tab.viewState) {
663
- editor.restoreViewState(tab.viewState);
669
+ // Read the view state from the store, not from the captured `tab` prop: this
670
+ // effect re-runs on collabReady, and the cleanup that just ran wrote a fresher
671
+ // snapshot than the prop this closure was created with.
672
+ var _liveTab = findTabByPath(tab.path);
673
+ var _viewState = (_liveTab && _liveTab.viewState) || tab.viewState;
674
+ if (_viewState) {
675
+ editor.restoreViewState(_viewState);
664
676
  }
665
677
 
666
678
  // Restore the find widget state from the previous editor (when persistFindState is on).
@@ -705,6 +717,32 @@ var EditorPanel = function EditorPanel(_ref) {
705
717
 
706
718
  monacoRef.current = editor;
707
719
  window.__mbeditorActiveEditor = editor;
720
+ // Publish the live editor instance so a markdown preview in another pane
721
+ // (a separate EditorPanel instance) can find it for scroll sync.
722
+ _modelEntry.editor = editor;
723
+
724
+ var scrollSyncDisposable = null;
725
+ if (/\.(md|markdown)$/i.test(tab.path || '')) {
726
+ scrollSyncDisposable = editor.onDidScrollChange(function () {
727
+ if (_modelEntry.scrollSyncing) return;
728
+ var scrollHeight = editor.getScrollHeight() - editor.getLayoutInfo().height;
729
+ if (scrollHeight <= 0) return;
730
+ var fraction = editor.getScrollTop() / scrollHeight;
731
+ var previews = document.querySelectorAll('[data-preview-for]');
732
+ for (var _p = 0; _p < previews.length; _p++) {
733
+ if (previews[_p].getAttribute('data-preview-for') !== tab.path) continue;
734
+ var max = previews[_p].scrollHeight - previews[_p].clientHeight;
735
+ if (max <= 0) continue;
736
+ _modelEntry.scrollSyncing = true;
737
+ previews[_p].scrollTop = fraction * max;
738
+ _modelEntry.scrollSyncing = false;
739
+ }
740
+ });
741
+ }
742
+ // Tells the status bar's cursor readout to re-attach. An event rather than
743
+ // a prop because the editor is published imperatively here, and a listener
744
+ // that guessed at the timing would miss the swap on a tab switch.
745
+ window.dispatchEvent(new CustomEvent('mbeditor:active-editor'));
708
746
  // Apply read-only for paginated (truncated) files
709
747
  if (tab.truncated) {
710
748
  editor.updateOptions({ readOnly: true });
@@ -768,14 +806,13 @@ var EditorPanel = function EditorPanel(_ref) {
768
806
  EditorStore.setState({ isQuickOpenVisible: true });
769
807
  });
770
808
 
771
- // PageUp/PageDown cycle between the current cursor position and where the
772
- // cursor was before the last jump (go-to-definition, search result, etc.),
773
- // replacing the default page-scroll behaviour.
809
+ // PageUp/PageDown walk a back/forward navigation history (go-to-definition,
810
+ // search results, file switches), replacing the default page-scroll behaviour.
774
811
  editor.addCommand(window.monaco.KeyCode.PageUp, function() {
775
- TabManager.toggleJumpOrigin();
812
+ TabManager.navigateBack();
776
813
  });
777
814
  editor.addCommand(window.monaco.KeyCode.PageDown, function() {
778
- TabManager.toggleJumpOrigin();
815
+ TabManager.navigateForward();
779
816
  });
780
817
 
781
818
  var editorPluginDisposable = null;
@@ -843,7 +880,25 @@ var EditorPanel = function EditorPanel(_ref) {
843
880
  // below can swap the model, so this must be re-attached to the replacement —
844
881
  // a listener left on the old model would silently stop tracking edits.
845
882
  var _attachContentListener = function (model) {
846
- return model.onDidChangeContent(function (e) {
883
+ // onDidChangeContent fires per keystroke, and the full-buffer work it used
884
+ // to do — one getValue() plus two whole-file CRLF normalisations — costs
885
+ // ~12ms per keystroke on a large file, on the main thread, ahead of
886
+ // Monaco's own processing. The AVI bookkeeping below stays per-event and
887
+ // O(1); the buffer is materialised once on a trailing edge (and once more
888
+ // by TabManager's own 250ms flush, via the provider passed to markDirty).
889
+ var _contentSyncTimer = null;
890
+ var _contentProvider = function () {
891
+ return model.isDisposed() ? null : model.getValue();
892
+ };
893
+ var _flushContentSync = function () {
894
+ _contentSyncTimer = null;
895
+ var val = _contentProvider();
896
+ if (val === null) return;
897
+ latestContentRef.current = val;
898
+ onContentChange(val);
899
+ };
900
+
901
+ var _contentListener = model.onDidChangeContent(function (e) {
847
902
  if (!_collabActive && typeof HistoryService !== 'undefined') {
848
903
  HistoryService.recordOps(tab.path, e.changes);
849
904
  }
@@ -861,8 +916,6 @@ var EditorPanel = function EditorPanel(_ref) {
861
916
  EditorStore.setState({ canUndo: newCanUndo, canRedo: newCanRedo });
862
917
  }
863
918
 
864
- var val = model.getValue();
865
-
866
919
  // Dirty-state tracking via alternativeVersionId — O(1), no string comparison.
867
920
  // AVI decrements on undo so it returns to cleanVersionId after a full undo.
868
921
  // Skip entirely when cleanVersionId is null — file is mid-load, not yet settled.
@@ -870,155 +923,179 @@ var EditorPanel = function EditorPanel(_ref) {
870
923
  var _cleanAvi = _entry && _entry.cleanVersionId;
871
924
  if (_cleanAvi !== null && _cleanAvi !== undefined) {
872
925
  if (currentAvi !== _cleanAvi) {
873
- TabManager.markDirty(paneId, tab.id, val);
926
+ TabManager.markDirty(paneId, tab.id, _contentProvider);
874
927
  } else {
875
- TabManager.markClean(paneId, tab.id, val);
928
+ TabManager.markClean(paneId, tab.id, _contentProvider);
876
929
  }
877
930
  }
878
931
 
879
- var currentContent = latestContentRef.current;
880
-
881
- // Normalize before comparing to prevent false positive dirty edits
882
- var vNorm = val.replace(/\r\n/g, '\n');
883
- var cNorm = currentContent.replace(/\r\n/g, '\n');
884
- if (vNorm !== cNorm) {
885
- // Update the ref immediately so rapid undo/redo events compare against the
886
- // latest content rather than a stale snapshot from a previous React render.
887
- latestContentRef.current = val;
888
- onContentChange(val);
932
+ // Draft persistence is the only remaining consumer of the full text.
933
+ // Coalesce it onto the same 250ms edge TabManager uses for content writes.
934
+ if (_contentSyncTimer === null) {
935
+ _contentSyncTimer = setTimeout(_flushContentSync, 250);
889
936
  }
890
937
  });
938
+ return {
939
+ dispose: function () {
940
+ if (_contentSyncTimer !== null) { clearTimeout(_contentSyncTimer); _contentSyncTimer = null; }
941
+ _contentListener.dispose();
942
+ }
943
+ };
891
944
  };
892
945
 
893
946
  var contentDisposable = _attachContentListener(modelObj);
894
947
 
895
948
  // Phase 2: background undo-history replay.
896
- // Only run for newly-created models (reused models already have their undo stack).
949
+ // Only run for newly-created models (reused models already have their undo
950
+ // stack). Tracking and replay both wait for the file load: the base is the
951
+ // loaded content, so the load is neither recorded nor buffered.
897
952
  var _phase2CleanupFn = null;
898
- if (!_collabActive && !_virtualPath && !_reusingModel && typeof HistoryService !== 'undefined') {
899
- var _phase2Branch = EditorStore.getState().gitBranch || '';
900
- var _phase2Path = tab.path;
901
- var _phase2Content = tab.content || '';
902
- var _phase2Buf = [];
903
- var _phase2Active = true;
904
-
905
- var _phase2ModelA = modelObj;
906
- var _phase2Listener = _phase2ModelA.onDidChangeContent(function (ev) {
907
- if (!_phase2Active) return;
908
- for (var _ci = 0; _ci < ev.changes.length; _ci++) {
909
- var _c = ev.changes[_ci];
910
- _phase2Buf.push([
911
- _c.range.startLineNumber, _c.range.startColumn,
912
- _c.range.endLineNumber, _c.range.endColumn,
913
- _c.text
914
- ]);
915
- }
916
- });
953
+ if (_historyEnabled && !_reusingModel) {
954
+ var _phase2Path = tab.path;
955
+ var _phase2Buf = [];
956
+ var _phase2Active = true;
957
+ var _phase2ModelA = modelObj;
958
+ var _phase2Listener = null;
959
+ var _historyStarted = false;
960
+ var _startHistory = null;
961
+
962
+ _phase2CleanupFn = function () {
963
+ _phase2Active = false;
964
+ if (_phase2Listener) _phase2Listener.dispose();
965
+ };
966
+
967
+ _startHistory = function () {
968
+ if (_historyStarted || !_phase2Active) return;
969
+ var _phase2Branch = EditorStore.getState().gitBranch || '';
970
+ if (!_phase2Branch) return;
971
+ _historyStarted = true;
917
972
 
918
- var _runPhase2 = function () {
919
- if (!_phase2Active || !_phase2Branch) return;
920
- HistoryService.fetchHistory(_phase2Branch, _phase2Path).then(function (hist) {
973
+ HistoryService.beginTracking(_phase2Branch, _phase2Path, _phase2ModelA.getValue());
974
+
975
+ // Buffer edits made on model A between the load and the replay swap so
976
+ // they can be re-applied to the replayed model B.
977
+ _phase2Listener = _phase2ModelA.onDidChangeContent(function (ev) {
921
978
  if (!_phase2Active) return;
922
- if (!hist || !hist.ops || hist.ops.length === 0) {
923
- _phase2Listener.dispose();
924
- return;
979
+ for (var _ci = 0; _ci < ev.changes.length; _ci++) {
980
+ var _c = ev.changes[_ci];
981
+ _phase2Buf.push([
982
+ _c.range.startLineNumber, _c.range.startColumn,
983
+ _c.range.endLineNumber, _c.range.endColumn,
984
+ _c.text
985
+ ]);
925
986
  }
987
+ });
926
988
 
927
- var _lang = modelObj.getLanguageId();
928
- var modelB = window.monaco.editor.createModel(hist.base, _lang);
929
-
930
- HistoryService.setReplayInProgress(_phase2Path, true);
931
- try {
932
- for (var _oi = 0; _oi < hist.ops.length; _oi++) {
933
- var _op = hist.ops[_oi];
934
- modelB.pushEditOperations([], [{
935
- range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
936
- text: _op[4] || ''
937
- }], function () { return null; });
989
+ var _runPhase2 = function () {
990
+ if (!_phase2Active) return;
991
+ HistoryService.fetchHistory(_phase2Branch, _phase2Path).then(function (hist) {
992
+ if (!_phase2Active) return;
993
+ if (!hist || !hist.ops || hist.ops.length === 0) {
994
+ if (_phase2Listener) _phase2Listener.dispose();
995
+ return;
938
996
  }
939
- } catch (e) {
940
- HistoryService.setReplayInProgress(_phase2Path, false);
941
- _phase2Listener.dispose();
942
- modelB.dispose();
943
- return;
944
- }
945
- HistoryService.setReplayInProgress(_phase2Path, false);
946
997
 
947
- var _expectedContent = _phase2ModelA.getValue();
948
- if (modelB.getValue() !== _expectedContent) {
949
- _phase2Listener.dispose();
950
- modelB.dispose();
951
- return;
952
- }
998
+ var _lang = modelObj.getLanguageId();
999
+ var modelB = window.monaco.editor.createModel(hist.base, _lang);
953
1000
 
954
- if (_phase2Buf.length > 0) {
1001
+ HistoryService.setReplayInProgress(_phase2Path, true);
955
1002
  try {
956
- for (var _bi = 0; _bi < _phase2Buf.length; _bi++) {
957
- var _bop = _phase2Buf[_bi];
1003
+ for (var _oi = 0; _oi < hist.ops.length; _oi++) {
1004
+ var _op = hist.ops[_oi];
958
1005
  modelB.pushEditOperations([], [{
959
- range: new window.monaco.Range(_bop[0], _bop[1], _bop[2], _bop[3]),
960
- text: _bop[4] || ''
1006
+ range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
1007
+ text: _op[4] || ''
961
1008
  }], function () { return null; });
962
1009
  }
963
1010
  } catch (e) {
964
- _phase2Listener.dispose();
1011
+ HistoryService.setReplayInProgress(_phase2Path, false);
1012
+ if (_phase2Listener) _phase2Listener.dispose();
965
1013
  modelB.dispose();
966
1014
  return;
967
1015
  }
968
- }
1016
+ HistoryService.setReplayInProgress(_phase2Path, false);
969
1017
 
970
- _phase2Listener.dispose();
971
- if (!_phase2Active) { modelB.dispose(); return; }
1018
+ // Fold in the edits made on model A while the replay was in flight,
1019
+ // THEN validate. Applying them after the check is what let a buffered
1020
+ // initial load slip through and duplicate the file (#92).
1021
+ if (_phase2Buf.length > 0) {
1022
+ try {
1023
+ for (var _bi = 0; _bi < _phase2Buf.length; _bi++) {
1024
+ var _bop = _phase2Buf[_bi];
1025
+ modelB.pushEditOperations([], [{
1026
+ range: new window.monaco.Range(_bop[0], _bop[1], _bop[2], _bop[3]),
1027
+ text: _bop[4] || ''
1028
+ }], function () { return null; });
1029
+ }
1030
+ } catch (e) {
1031
+ if (_phase2Listener) _phase2Listener.dispose();
1032
+ modelB.dispose();
1033
+ return;
1034
+ }
1035
+ }
972
1036
 
973
- if (modelB.getLanguageId() !== _lang) {
974
- window.monaco.editor.setModelLanguage(modelB, _lang);
975
- }
976
- modelB._mbeditorPath = _phase2Path;
977
-
978
- var _vs = editor.saveViewState();
979
- editor.setModel(modelB);
980
- if (_vs) editor.restoreViewState(_vs);
981
-
982
- var _oldEntry = window.__mbeditorModels[_phase2Path];
983
- if (_oldEntry && _oldEntry.model !== modelB) {
984
- var _oldModel = _oldEntry.model;
985
- // modelB holds the same text as modelA but its own version-id sequence,
986
- // so re-anchor the clean baseline. Carry the dirty state across: when the
987
- // tab was already dirty, -1 can never match a real AVI, so it stays dirty.
988
- var _oldClean = _oldEntry.cleanVersionId;
989
- var _wasDirty = _oldClean !== null && _oldClean !== undefined &&
990
- _oldModel.getAlternativeVersionId() !== _oldClean;
991
- var _newAvi = modelB.getAlternativeVersionId();
992
- window.__mbeditorModels[_phase2Path] = {
993
- model: modelB,
994
- aviBase: aviBaseRef.current,
995
- aviMax: _newAvi,
996
- lastAccessed: Date.now(),
997
- cleanVersionId: _wasDirty ? -1 : _newAvi
998
- };
999
- aviMaxRef.current = _newAvi;
1000
- setTimeout(function () {
1001
- if (_oldModel && !_oldModel.isDisposed()) _oldModel.dispose();
1002
- }, 0);
1003
- }
1037
+ if (modelB.getValue() !== _phase2ModelA.getValue()) {
1038
+ if (_phase2Listener) _phase2Listener.dispose();
1039
+ modelB.dispose();
1040
+ return;
1041
+ }
1004
1042
 
1005
- // Move the content listener onto modelB. It carries dirty tracking, content
1006
- // sync and history recording — leaving it on modelA strands all three.
1007
- contentDisposable.dispose();
1008
- contentDisposable = _attachContentListener(modelB);
1009
- _phase2CleanupFn = function () { _phase2Active = false; };
1010
- }).catch(function () {
1011
- _phase2Listener.dispose();
1012
- });
1043
+ if (_phase2Listener) _phase2Listener.dispose();
1044
+ if (!_phase2Active) { modelB.dispose(); return; }
1045
+
1046
+ if (modelB.getLanguageId() !== _lang) {
1047
+ window.monaco.editor.setModelLanguage(modelB, _lang);
1048
+ }
1049
+ modelB._mbeditorPath = _phase2Path;
1050
+
1051
+ var _vs = editor.saveViewState();
1052
+ editor.setModel(modelB);
1053
+ if (_vs) editor.restoreViewState(_vs);
1054
+
1055
+ var _oldEntry = window.__mbeditorModels[_phase2Path];
1056
+ if (_oldEntry && _oldEntry.model !== modelB) {
1057
+ var _oldModel = _oldEntry.model;
1058
+ // modelB holds the same text as modelA but its own version-id sequence,
1059
+ // so re-anchor the clean baseline. Carry the dirty state across: when the
1060
+ // tab was already dirty, -1 can never match a real AVI, so it stays dirty.
1061
+ var _oldClean = _oldEntry.cleanVersionId;
1062
+ var _wasDirty = _oldClean !== null && _oldClean !== undefined &&
1063
+ _oldModel.getAlternativeVersionId() !== _oldClean;
1064
+ var _newAvi = modelB.getAlternativeVersionId();
1065
+ window.__mbeditorModels[_phase2Path] = {
1066
+ model: modelB,
1067
+ aviBase: aviBaseRef.current,
1068
+ aviMax: _newAvi,
1069
+ lastAccessed: Date.now(),
1070
+ cleanVersionId: _wasDirty ? -1 : _newAvi
1071
+ };
1072
+ aviMaxRef.current = _newAvi;
1073
+ setTimeout(function () {
1074
+ if (_oldModel && !_oldModel.isDisposed()) _oldModel.dispose();
1075
+ }, 0);
1076
+ }
1077
+
1078
+ // Move the content listener onto modelB. It carries dirty tracking, content
1079
+ // sync and history recording — leaving it on modelA strands all three.
1080
+ contentDisposable.dispose();
1081
+ contentDisposable = _attachContentListener(modelB);
1082
+ }).catch(function () {
1083
+ if (_phase2Listener) _phase2Listener.dispose();
1084
+ });
1085
+ };
1086
+
1087
+ if (typeof requestIdleCallback !== 'undefined') {
1088
+ requestIdleCallback(_runPhase2, { timeout: 2000 });
1089
+ } else {
1090
+ setTimeout(_runPhase2, 200);
1091
+ }
1013
1092
  };
1014
1093
 
1015
- if (typeof requestIdleCallback !== 'undefined') {
1016
- requestIdleCallback(_runPhase2, { timeout: 2000 });
1094
+ if (_contentLoaded) {
1095
+ _startHistory();
1017
1096
  } else {
1018
- setTimeout(_runPhase2, 200);
1097
+ pendingHistorySetupRef.current = _startHistory;
1019
1098
  }
1020
-
1021
- _phase2CleanupFn = function () { _phase2Active = false; _phase2Listener.dispose(); };
1022
1099
  }
1023
1100
 
1024
1101
  return function () {
@@ -1055,15 +1132,19 @@ var EditorPanel = function EditorPanel(_ref) {
1055
1132
  }
1056
1133
  if (window.__mbeditorActiveEditor === editor) {
1057
1134
  window.__mbeditorActiveEditor = null;
1135
+ window.dispatchEvent(new CustomEvent('mbeditor:active-editor'));
1058
1136
  }
1137
+ if (scrollSyncDisposable) scrollSyncDisposable.dispose();
1138
+ if (_modelEntry.editor === editor) _modelEntry.editor = null;
1059
1139
  if (editorPluginDisposable) editorPluginDisposable.dispose();
1060
1140
  if (formatActionDisposable) formatActionDisposable.dispose();
1061
1141
  runTestAtCursorDisposable.dispose();
1062
1142
  columnSelectDisposable.dispose();
1063
1143
  contentDisposable.dispose();
1064
1144
  EditorStore.setState({ canUndo: false, canRedo: false });
1145
+ pendingHistorySetupRef.current = null;
1065
1146
  if (_phase2CleanupFn) _phase2CleanupFn();
1066
- if (_collabActive) CollaborationService.unbindEditor(tab.path);
1147
+ if (_collabActive) CollaborationService.unbindEditor(tab.path, editor);
1067
1148
  // Detach the model before disposing the editor so the model (and its undo
1068
1149
  // history) survives for when the user returns to this tab.
1069
1150
  editor.setModel(null);
@@ -1078,14 +1159,29 @@ var EditorPanel = function EditorPanel(_ref) {
1078
1159
  var editor = monacoRef.current;
1079
1160
  if (!editor || typeof tab.content !== 'string') return;
1080
1161
 
1162
+ // The editor effect defers persistent-undo tracking until the file load
1163
+ // lands (see pendingHistorySetupRef). Start it here, after the content has
1164
+ // been applied, so the loaded file is the history base rather than an op.
1165
+ var _runPendingHistory = function () {
1166
+ var pending = pendingHistorySetupRef.current;
1167
+ if (pending) {
1168
+ pendingHistorySetupRef.current = null;
1169
+ pending();
1170
+ }
1171
+ };
1172
+
1081
1173
  var extVersion = tab.externalContentVersion || 0;
1082
- if (extVersion <= lastAppliedExternalVersionRef.current) return;
1174
+ if (extVersion <= lastAppliedExternalVersionRef.current) {
1175
+ _runPendingHistory();
1176
+ return;
1177
+ }
1083
1178
 
1084
1179
  // For a collaboration late-join the shared document is authoritative; applying
1085
1180
  // the disk content would clobber it. Mark this version consumed and skip.
1086
1181
  if (collabActiveRef.current && typeof CollaborationService !== 'undefined' &&
1087
1182
  CollaborationService.consumesDiskLoad(tab.path)) {
1088
1183
  lastAppliedExternalVersionRef.current = extVersion;
1184
+ _runPendingHistory();
1089
1185
  return;
1090
1186
  }
1091
1187
 
@@ -1098,39 +1194,48 @@ var EditorPanel = function EditorPanel(_ref) {
1098
1194
  // Normalize before comparing to prevent false positive dirty edits
1099
1195
  var vNorm = editor.getValue().replace(/\r\n/g, '\n');
1100
1196
  var cNorm = tab.content.replace(/\r\n/g, '\n');
1101
- if (vNorm === cNorm) return;
1102
-
1103
- if (!vNorm) {
1104
- // If the editor is currently completely empty, treat it as an initial load.
1105
- // setValue clears the undo stack which is correct for initial load.
1106
- // Null cleanVersionId before setValue so the synchronous onDidChangeContent
1107
- // fires during setValue and skips the dirty check (cleanVersionId is null).
1108
- var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1109
- if (_initEntry) _initEntry.cleanVersionId = null;
1110
-
1111
- editor.setValue(tab.content);
1112
- // Reset the AVI baseline: setValue clears the undo stack so anything before
1113
- // this point is no longer reachable. Also clear the canUndo/canRedo display.
1114
- var newBase = model.getAlternativeVersionId();
1115
- aviBaseRef.current = newBase;
1116
- aviMaxRef.current = newBase;
1117
- if (_initEntry) _initEntry.cleanVersionId = newBase;
1118
- EditorStore.setState({ canUndo: false, canRedo: false });
1119
- } else {
1120
- // Keep undo stack for formats or replaces by using executeEdits
1121
- var _extEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1122
- if (_extEntry) _extEntry.cleanVersionId = null;
1123
- editor.pushUndoStop();
1124
- editor.executeEdits("external", [{
1125
- range: model.getFullModelRange(),
1126
- text: tab.content
1127
- }]);
1128
- editor.pushUndoStop();
1129
- // Re-anchor the clean baseline so onDidChangeContent doesn't mark this
1130
- // externally-applied content as a dirty edit.
1131
- var _newExtAvi = model.getAlternativeVersionId();
1132
- if (_extEntry) _extEntry.cleanVersionId = _newExtAvi;
1133
- aviBaseRef.current = _newExtAvi;
1197
+ if (vNorm !== cNorm) {
1198
+ if (!vNorm) {
1199
+ // If the editor is currently completely empty, treat it as an initial load.
1200
+ // setValue clears the undo stack which is correct for initial load.
1201
+ // Null cleanVersionId before setValue so the synchronous onDidChangeContent
1202
+ // fires during setValue and skips the dirty check (cleanVersionId is null).
1203
+ var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1204
+ if (_initEntry) _initEntry.cleanVersionId = null;
1205
+
1206
+ editor.setValue(tab.content);
1207
+ // Reset the AVI baseline: setValue clears the undo stack so anything before
1208
+ // this point is no longer reachable. Also clear the canUndo/canRedo display.
1209
+ var newBase = model.getAlternativeVersionId();
1210
+ aviBaseRef.current = newBase;
1211
+ aviMaxRef.current = newBase;
1212
+ if (_initEntry) _initEntry.cleanVersionId = newBase;
1213
+ EditorStore.setState({ canUndo: false, canRedo: false });
1214
+ } else {
1215
+ // Keep undo stack for formats or replaces by using executeEdits
1216
+ var _extEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1217
+ if (_extEntry) _extEntry.cleanVersionId = null;
1218
+ editor.pushUndoStop();
1219
+ editor.executeEdits("external", [{
1220
+ range: model.getFullModelRange(),
1221
+ text: tab.content
1222
+ }]);
1223
+ editor.pushUndoStop();
1224
+ // Re-anchor the clean baseline so onDidChangeContent doesn't mark this
1225
+ // externally-applied content as a dirty edit.
1226
+ var _newExtAvi = model.getAlternativeVersionId();
1227
+ if (_extEntry) _extEntry.cleanVersionId = _newExtAvi;
1228
+ aviBaseRef.current = _newExtAvi;
1229
+ }
1230
+ }
1231
+
1232
+ // Empty file: content already matches, but tracking still has to start.
1233
+ _runPendingHistory();
1234
+
1235
+ // The model now holds the disk content. A room that deferred attaching while
1236
+ // this fetch was in flight can seed the shared doc from it.
1237
+ if (collabActiveRef.current && typeof CollaborationService !== 'undefined') {
1238
+ CollaborationService.contentReady(tab.path);
1134
1239
  }
1135
1240
  }, [tab.content, tab.externalContentVersion]);
1136
1241
 
@@ -1216,7 +1321,7 @@ var EditorPanel = function EditorPanel(_ref) {
1216
1321
  formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
1217
1322
  quickSuggestions: editorPrefs.quickSuggestions !== false,
1218
1323
  wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
1219
- linkedEditing: !!(editorPrefs.linkedEditing),
1324
+ linkedEditing: true,
1220
1325
  acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on'
1221
1326
  });
1222
1327
  // Re-apply large-file tuning last so a prefs change can't re-enable the
@@ -1694,9 +1799,8 @@ var EditorPanel = function EditorPanel(_ref) {
1694
1799
  // keystroke. This ticks 250 ms after the last change instead — the same
1695
1800
  // treatment the route-hint redraw above already gets.
1696
1801
  var _useStateZT = useState(0);
1697
- var _useStateZT2 = _slicedToArray(_useStateZT, 2);
1698
- var zoneTick = _useStateZT2[0];
1699
- var setZoneTick = _useStateZT2[1];
1802
+ var zoneTick = _useStateZT[0];
1803
+ var setZoneTick = _useStateZT[1];
1700
1804
  useEffect(function () {
1701
1805
  var timer = setTimeout(function () {
1702
1806
  setZoneTick(function (n) { return n + 1; });
@@ -1966,33 +2070,71 @@ var EditorPanel = function EditorPanel(_ref) {
1966
2070
  if (isMarkdown && window.marked) {
1967
2071
  (function () {
1968
2072
  var renderer = new window.marked.Renderer();
1969
- var escapeHtml = function escapeHtml(str) {
1970
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1971
- };
2073
+ // escapeHtml and safeHref are defined once in ChangelogView.js — the
2074
+ // href allow-list guards both markdown surfaces and must not fork.
2075
+ var escapeHtml = window.mbeditorEscapeHtml;
2076
+ var safeHref = window.mbeditorSafeHref;
1972
2077
  renderer.html = function (token) {
1973
2078
  return '<pre>' + escapeHtml(typeof token === 'object' ? token.text : token) + '</pre>';
1974
2079
  };
1975
- // Sanitize link/image hrefs to block javascript: and data: schemes.
1976
2080
  var _origLink = renderer.link.bind(renderer);
1977
2081
  var _origImage = renderer.image.bind(renderer);
1978
- var SAFE_HREF_SCHEME = /^(https?:|mailto:|#|\/)/i;
1979
- var safeHref = function(href) {
1980
- if (!href) return href;
1981
- return SAFE_HREF_SCHEME.test(href.trim()) ? href : '#';
2082
+ // marked 10 calls these positionally — `.link(href, title, text)`, see
2083
+ // the vendored bundle. An earlier version took a token object, and
2084
+ // against the real signature the object branch never ran: safeHref was
2085
+ // dead, so javascript: hrefs passed through, and dropping title/text
2086
+ // rendered every link as "undefined".
2087
+ renderer.link = function(href, title, text) {
2088
+ return _origLink(safeHref(href), title, text);
1982
2089
  };
1983
- renderer.link = function(token) {
1984
- if (token && typeof token === 'object') { token = Object.assign({}, token, { href: safeHref(token.href) }); }
1985
- return _origLink(token);
2090
+ renderer.image = function(href, title, text) {
2091
+ return _origImage(safeHref(href), title, text);
1986
2092
  };
1987
- renderer.image = function(token) {
1988
- if (token && typeof token === 'object') { token = Object.assign({}, token, { href: safeHref(token.href) }); }
1989
- return _origImage(token);
2093
+ // marked 10 dropped heading ids, so every in-document `[x](#anchor)`
2094
+ // link pointed at nothing. Slugs follow GitHub's: strip markup, lower,
2095
+ // drop punctuation, spaces to hyphens, `-1`/`-2` for repeats.
2096
+ var slugSeen = {};
2097
+ renderer.heading = function (text, level) {
2098
+ var slug = String(text)
2099
+ .replace(/<[^>]+>/g, '')
2100
+ .replace(/&[#\w]+;/g, '')
2101
+ .trim().toLowerCase()
2102
+ .replace(/[^\w\- ]+/g, '')
2103
+ .replace(/\s/g, '-');
2104
+ slugSeen[slug] = (slugSeen[slug] || 0) + 1;
2105
+ if (slugSeen[slug] > 1) slug += '-' + (slugSeen[slug] - 1);
2106
+ return '<h' + level + ' id="' + slug + '">' + text + '</h' + level + '>\n';
1990
2107
  };
1991
2108
  setMarkup(window.marked.parse(markdownContent, { renderer: renderer }));
1992
2109
  })();
1993
2110
  }
1994
2111
  }, [markdownContent, isMarkdown]);
1995
2112
 
2113
+ // Preview -> source scroll sync. The source editor instance (a different
2114
+ // EditorPanel, possibly a different pane) is published on the shared model
2115
+ // entry by the editor-creation effect above; scrollSyncing on that same
2116
+ // entry is the shared flag that stops the two directions ping-ponging.
2117
+ useEffect(function () {
2118
+ if (!tab.isPreview || !isMarkdown) return;
2119
+ var el = previewScrollRef.current;
2120
+ if (!el) return;
2121
+ function onScroll() {
2122
+ var modelEntry = window.__mbeditorModels && window.__mbeditorModels[sourcePath];
2123
+ var editor = modelEntry && modelEntry.editor;
2124
+ if (!editor || (modelEntry && modelEntry.scrollSyncing)) return;
2125
+ var max = el.scrollHeight - el.clientHeight;
2126
+ if (max <= 0) return;
2127
+ var fraction = el.scrollTop / max;
2128
+ var scrollHeight = editor.getScrollHeight() - editor.getLayoutInfo().height;
2129
+ if (scrollHeight <= 0) return;
2130
+ modelEntry.scrollSyncing = true;
2131
+ editor.setScrollTop(fraction * scrollHeight);
2132
+ modelEntry.scrollSyncing = false;
2133
+ }
2134
+ el.addEventListener('scroll', onScroll);
2135
+ return function () { el.removeEventListener('scroll', onScroll); };
2136
+ }, [tab.isPreview, isMarkdown, sourcePath]);
2137
+
1996
2138
  // Click-outside handler to close the methods dropdown
1997
2139
  useEffect(function() {
1998
2140
  if (!methodsOpen) return;
@@ -2102,7 +2244,12 @@ var EditorPanel = function EditorPanel(_ref) {
2102
2244
  }
2103
2245
 
2104
2246
  if (tab.isPreview && isMarkdown) {
2105
- return React.createElement('div', { className: 'markdown-preview markdown-preview-full', dangerouslySetInnerHTML: { __html: markup } });
2247
+ return React.createElement('div', {
2248
+ className: 'markdown-preview markdown-preview-full',
2249
+ ref: previewScrollRef,
2250
+ 'data-preview-for': sourcePath,
2251
+ dangerouslySetInnerHTML: { __html: markup }
2252
+ });
2106
2253
  }
2107
2254
 
2108
2255
  // Helper: shorten long paths by showing the last 2 segments with a leading ellipsis
@@ -2113,6 +2260,38 @@ var EditorPanel = function EditorPanel(_ref) {
2113
2260
  return '\u2026/' + parts.slice(-2).join('/');
2114
2261
  }
2115
2262
 
2263
+ // Breadcrumb: shortPath()'s segments, chevron-separated, the final one an
2264
+ // icon + filename in the normal text colour, everything before it muted \u2014
2265
+ // VS Code's editor breadcrumb. Look only; not a navigation control.
2266
+ function renderBreadcrumb(path) {
2267
+ var segments = shortPath(path).split('/');
2268
+ var last = segments.length - 1;
2269
+ var nodes = [];
2270
+ segments.forEach(function (seg, i) {
2271
+ if (i > 0) {
2272
+ nodes.push(React.createElement('i', {
2273
+ key: 'sep-' + i,
2274
+ className: 'fas fa-chevron-right ide-breadcrumb-sep',
2275
+ 'aria-hidden': 'true'
2276
+ }));
2277
+ }
2278
+ if (i === last) {
2279
+ nodes.push(React.createElement(
2280
+ 'span',
2281
+ { key: 'seg-' + i, className: 'ide-breadcrumb-file' },
2282
+ React.createElement('i', {
2283
+ className: (window.getFileIcon ? window.getFileIcon(path) : 'far fa-file-code') + ' ide-breadcrumb-file-icon',
2284
+ 'aria-hidden': 'true'
2285
+ }),
2286
+ seg
2287
+ ));
2288
+ } else {
2289
+ nodes.push(React.createElement('span', { key: 'seg-' + i, className: 'ide-breadcrumb-dir' }, seg));
2290
+ }
2291
+ });
2292
+ return nodes;
2293
+ }
2294
+
2116
2295
  // While Monaco is still loading, show a lightweight skeleton so the UI is
2117
2296
  // visible immediately without calling monaco.editor.create() too early.
2118
2297
  if (!monacoReady) {
@@ -2191,7 +2370,7 @@ var EditorPanel = function EditorPanel(_ref) {
2191
2370
  React.createElement(
2192
2371
  'span',
2193
2372
  { className: 'ide-editor-file-location', title: tab.path },
2194
- shortPath(tab.path)
2373
+ renderBreadcrumb(tab.path)
2195
2374
  ),
2196
2375
  gitAvailable && tab.path && React.createElement(
2197
2376
  'button',
@@ -2200,8 +2379,8 @@ var EditorPanel = function EditorPanel(_ref) {
2200
2379
  onClick: function() { if (onShowHistory) onShowHistory(tab.path); },
2201
2380
  title: 'File History'
2202
2381
  },
2203
- React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
2204
- !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
2382
+ React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
2383
+ editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
2205
2384
  ),
2206
2385
  hasOutline && React.createElement(
2207
2386
  'button',
@@ -2255,8 +2434,8 @@ var EditorPanel = function EditorPanel(_ref) {
2255
2434
  },
2256
2435
  title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
2257
2436
  },
2258
- React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
2259
- !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isTestOutline ? 'Outline' : 'Methods')
2437
+ React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
2438
+ editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, isTestOutline ? 'Outline' : 'Methods')
2260
2439
  ),
2261
2440
  gitAvailable && React.createElement(
2262
2441
  'button',
@@ -2265,8 +2444,8 @@ var EditorPanel = function EditorPanel(_ref) {
2265
2444
  onClick: function() { setIsBlameVisible(function(prev) { return !prev; }); },
2266
2445
  title: 'Toggle Git Blame'
2267
2446
  },
2268
- React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
2269
- !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
2447
+ React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
2448
+ editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
2270
2449
  ),
2271
2450
  ),
2272
2451
  conflictCount > 0 && React.createElement(
@@ -2386,7 +2565,7 @@ var EditorPanel = function EditorPanel(_ref) {
2386
2565
  React.createElement(
2387
2566
  'span',
2388
2567
  { style: { flex: 1, textAlign: 'center' } },
2389
- 'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + humanSize(pageTotalBytes) + ')'
2568
+ 'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + formatSize(pageTotalBytes) + ')'
2390
2569
  ),
2391
2570
  React.createElement(
2392
2571
  'button',
@@ -2428,7 +2607,10 @@ var EditorPanel = function EditorPanel(_ref) {
2428
2607
  )
2429
2608
  ),
2430
2609
  React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
2431
- methodsOpen && methodsDropdownPos && React.createElement(
2610
+ // Portalled to body: under the glass chrome the centre column has a
2611
+ // backdrop-filter, which makes it the containing block for position:fixed
2612
+ // and shoves the menu ~130px off its button.
2613
+ methodsOpen && methodsDropdownPos && ReactDOM.createPortal(React.createElement(
2432
2614
  'div',
2433
2615
  {
2434
2616
  ref: methodsDropdownRef,
@@ -2526,7 +2708,7 @@ var EditorPanel = function EditorPanel(_ref) {
2526
2708
  }
2527
2709
  return rows;
2528
2710
  })()
2529
- ),
2711
+ ), document.body),
2530
2712
  React.createElement('div', { ref: vimStatusRef, className: 'vim-statusbar', style: { display: editorPrefs.vimMode ? 'flex' : 'none', height: '22px', alignItems: 'center', padding: '0 10px', fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", fontSize: '12px', background: 'var(--ide-statusbar-bg, #1e1e2e)', color: 'var(--ide-statusbar-fg, #9cdcfe)', borderTop: '1px solid var(--ide-border, #3e3e3e)', flexShrink: 0, userSelect: 'none', letterSpacing: '0.02em' } })
2531
2713
  );
2532
2714
  };