mbeditor 0.13.1 → 0.14.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +132 -0
- data/app/assets/javascripts/mbeditor/application.js +5 -0
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
- data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +442 -248
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
- data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1241 -1256
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
- data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +18 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +705 -323
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +48 -39
- data/app/assets/javascripts/mbeditor/git_service.js +15 -1
- data/app/assets/javascripts/mbeditor/history_service.js +5 -13
- data/app/assets/javascripts/mbeditor/search_service.js +9 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
- data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
- data/app/assets/stylesheets/mbeditor/application.css +6 -1
- data/app/assets/stylesheets/mbeditor/editor.css +672 -249
- data/app/assets/stylesheets/mbeditor/glass.css +174 -0
- data/app/assets/stylesheets/mbeditor/themes.css +93 -33
- data/app/channels/mbeditor/collaboration_channel.rb +17 -4
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +124 -439
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
- data/app/services/mbeditor/editor_state_service.rb +14 -51
- data/app/services/mbeditor/file_history_service.rb +222 -0
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
- data/app/services/mbeditor/lint_service.rb +137 -0
- data/app/services/mbeditor/locked_json_file.rb +67 -0
- data/app/services/mbeditor/process_runner.rb +32 -0
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
- data/app/services/mbeditor/search_replace_service.rb +8 -0
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -1
- data/lib/mbeditor/rack/pending_migration_bypass.rb +31 -10
- data/lib/mbeditor/route_map.rb +5 -0
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/public/monaco-editor/VERSIONS +1 -1
- data/public/monaco-editor/codicon.ttf +0 -0
- data/public/monaco-editor/css.worker.js +41 -37
- data/public/monaco-editor/editor.worker.js +19 -15
- data/public/monaco-editor/html.worker.js +53 -49
- data/public/monaco-editor/json.worker.js +36 -32
- data/public/monaco-editor/monaco.css +1 -1
- data/public/monaco-editor/monaco.js +388 -301
- data/public/monaco-editor/ts.worker.js +226 -222
- data/vendor/assets/javascripts/axios.min.js +3 -3
- metadata +15 -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
|
|
71
|
-
var
|
|
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
|
|
76
|
-
var
|
|
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
|
|
81
|
-
var
|
|
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
|
|
86
|
-
var
|
|
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
|
|
101
|
-
var
|
|
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
|
|
106
|
-
var
|
|
107
|
-
var setMethodsOpen = _useState12[1];
|
|
104
|
+
var methodsOpen = _useState11[0];
|
|
105
|
+
var setMethodsOpen = _useState11[1];
|
|
108
106
|
|
|
109
107
|
var _useState13 = useState([]);
|
|
110
|
-
var
|
|
111
|
-
var
|
|
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
|
|
116
|
-
var
|
|
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
|
|
121
|
-
var
|
|
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
|
|
126
|
-
var
|
|
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
|
|
137
|
-
var
|
|
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
|
|
142
|
-
var
|
|
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
|
|
147
|
-
var
|
|
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
|
|
152
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
662
|
+
linkedEditing: true,
|
|
656
663
|
fixedOverflowWidgets: true,
|
|
657
664
|
hover: { above: false }
|
|
658
665
|
});
|
|
659
666
|
|
|
660
667
|
applyLargeFileTuning(editor, modelObj);
|
|
661
668
|
|
|
662
|
-
|
|
663
|
-
|
|
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 });
|
|
@@ -754,6 +792,17 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
754
792
|
}
|
|
755
793
|
});
|
|
756
794
|
|
|
795
|
+
// Paste & Place: Ruby only. An editor action gets the context-menu entry and
|
|
796
|
+
// the F1 command palette listing without another toolbar button.
|
|
797
|
+
var pastePlaceDisposable = editor.addAction({
|
|
798
|
+
id: 'mbeditor.pastePlace',
|
|
799
|
+
label: 'Paste & Place Ruby Method...',
|
|
800
|
+
precondition: 'editorLangId == ruby',
|
|
801
|
+
contextMenuGroupId: 'navigation',
|
|
802
|
+
contextMenuOrder: 1.7,
|
|
803
|
+
run: function() { EditorStore.setState({ isPastePlaceVisible: true }); }
|
|
804
|
+
});
|
|
805
|
+
|
|
757
806
|
// No custom "Format Document" action: Ruby now has a real formatting
|
|
758
807
|
// provider, so Monaco contributes that context-menu entry itself, and a
|
|
759
808
|
// second one of our own showed up as a duplicate. Languages with no
|
|
@@ -768,14 +817,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
768
817
|
EditorStore.setState({ isQuickOpenVisible: true });
|
|
769
818
|
});
|
|
770
819
|
|
|
771
|
-
// PageUp/PageDown
|
|
772
|
-
//
|
|
773
|
-
// replacing the default page-scroll behaviour.
|
|
820
|
+
// PageUp/PageDown walk a back/forward navigation history (go-to-definition,
|
|
821
|
+
// search results, file switches), replacing the default page-scroll behaviour.
|
|
774
822
|
editor.addCommand(window.monaco.KeyCode.PageUp, function() {
|
|
775
|
-
TabManager.
|
|
823
|
+
TabManager.navigateBack();
|
|
776
824
|
});
|
|
777
825
|
editor.addCommand(window.monaco.KeyCode.PageDown, function() {
|
|
778
|
-
TabManager.
|
|
826
|
+
TabManager.navigateForward();
|
|
779
827
|
});
|
|
780
828
|
|
|
781
829
|
var editorPluginDisposable = null;
|
|
@@ -843,7 +891,25 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
843
891
|
// below can swap the model, so this must be re-attached to the replacement —
|
|
844
892
|
// a listener left on the old model would silently stop tracking edits.
|
|
845
893
|
var _attachContentListener = function (model) {
|
|
846
|
-
|
|
894
|
+
// onDidChangeContent fires per keystroke, and the full-buffer work it used
|
|
895
|
+
// to do — one getValue() plus two whole-file CRLF normalisations — costs
|
|
896
|
+
// ~12ms per keystroke on a large file, on the main thread, ahead of
|
|
897
|
+
// Monaco's own processing. The AVI bookkeeping below stays per-event and
|
|
898
|
+
// O(1); the buffer is materialised once on a trailing edge (and once more
|
|
899
|
+
// by TabManager's own 250ms flush, via the provider passed to markDirty).
|
|
900
|
+
var _contentSyncTimer = null;
|
|
901
|
+
var _contentProvider = function () {
|
|
902
|
+
return model.isDisposed() ? null : model.getValue();
|
|
903
|
+
};
|
|
904
|
+
var _flushContentSync = function () {
|
|
905
|
+
_contentSyncTimer = null;
|
|
906
|
+
var val = _contentProvider();
|
|
907
|
+
if (val === null) return;
|
|
908
|
+
latestContentRef.current = val;
|
|
909
|
+
onContentChange(val);
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
var _contentListener = model.onDidChangeContent(function (e) {
|
|
847
913
|
if (!_collabActive && typeof HistoryService !== 'undefined') {
|
|
848
914
|
HistoryService.recordOps(tab.path, e.changes);
|
|
849
915
|
}
|
|
@@ -861,8 +927,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
861
927
|
EditorStore.setState({ canUndo: newCanUndo, canRedo: newCanRedo });
|
|
862
928
|
}
|
|
863
929
|
|
|
864
|
-
var val = model.getValue();
|
|
865
|
-
|
|
866
930
|
// Dirty-state tracking via alternativeVersionId — O(1), no string comparison.
|
|
867
931
|
// AVI decrements on undo so it returns to cleanVersionId after a full undo.
|
|
868
932
|
// Skip entirely when cleanVersionId is null — file is mid-load, not yet settled.
|
|
@@ -870,155 +934,179 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
870
934
|
var _cleanAvi = _entry && _entry.cleanVersionId;
|
|
871
935
|
if (_cleanAvi !== null && _cleanAvi !== undefined) {
|
|
872
936
|
if (currentAvi !== _cleanAvi) {
|
|
873
|
-
TabManager.markDirty(paneId, tab.id,
|
|
937
|
+
TabManager.markDirty(paneId, tab.id, _contentProvider);
|
|
874
938
|
} else {
|
|
875
|
-
TabManager.markClean(paneId, tab.id,
|
|
939
|
+
TabManager.markClean(paneId, tab.id, _contentProvider);
|
|
876
940
|
}
|
|
877
941
|
}
|
|
878
942
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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);
|
|
943
|
+
// Draft persistence is the only remaining consumer of the full text.
|
|
944
|
+
// Coalesce it onto the same 250ms edge TabManager uses for content writes.
|
|
945
|
+
if (_contentSyncTimer === null) {
|
|
946
|
+
_contentSyncTimer = setTimeout(_flushContentSync, 250);
|
|
889
947
|
}
|
|
890
948
|
});
|
|
949
|
+
return {
|
|
950
|
+
dispose: function () {
|
|
951
|
+
if (_contentSyncTimer !== null) { clearTimeout(_contentSyncTimer); _contentSyncTimer = null; }
|
|
952
|
+
_contentListener.dispose();
|
|
953
|
+
}
|
|
954
|
+
};
|
|
891
955
|
};
|
|
892
956
|
|
|
893
957
|
var contentDisposable = _attachContentListener(modelObj);
|
|
894
958
|
|
|
895
959
|
// Phase 2: background undo-history replay.
|
|
896
|
-
// Only run for newly-created models (reused models already have their undo
|
|
960
|
+
// Only run for newly-created models (reused models already have their undo
|
|
961
|
+
// stack). Tracking and replay both wait for the file load: the base is the
|
|
962
|
+
// loaded content, so the load is neither recorded nor buffered.
|
|
897
963
|
var _phase2CleanupFn = null;
|
|
898
|
-
if (
|
|
899
|
-
var
|
|
900
|
-
var
|
|
901
|
-
var
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
964
|
+
if (_historyEnabled && !_reusingModel) {
|
|
965
|
+
var _phase2Path = tab.path;
|
|
966
|
+
var _phase2Buf = [];
|
|
967
|
+
var _phase2Active = true;
|
|
968
|
+
var _phase2ModelA = modelObj;
|
|
969
|
+
var _phase2Listener = null;
|
|
970
|
+
var _historyStarted = false;
|
|
971
|
+
var _startHistory = null;
|
|
972
|
+
|
|
973
|
+
_phase2CleanupFn = function () {
|
|
974
|
+
_phase2Active = false;
|
|
975
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
_startHistory = function () {
|
|
979
|
+
if (_historyStarted || !_phase2Active) return;
|
|
980
|
+
var _phase2Branch = EditorStore.getState().gitBranch || '';
|
|
981
|
+
if (!_phase2Branch) return;
|
|
982
|
+
_historyStarted = true;
|
|
917
983
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
984
|
+
HistoryService.beginTracking(_phase2Branch, _phase2Path, _phase2ModelA.getValue());
|
|
985
|
+
|
|
986
|
+
// Buffer edits made on model A between the load and the replay swap so
|
|
987
|
+
// they can be re-applied to the replayed model B.
|
|
988
|
+
_phase2Listener = _phase2ModelA.onDidChangeContent(function (ev) {
|
|
921
989
|
if (!_phase2Active) return;
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
990
|
+
for (var _ci = 0; _ci < ev.changes.length; _ci++) {
|
|
991
|
+
var _c = ev.changes[_ci];
|
|
992
|
+
_phase2Buf.push([
|
|
993
|
+
_c.range.startLineNumber, _c.range.startColumn,
|
|
994
|
+
_c.range.endLineNumber, _c.range.endColumn,
|
|
995
|
+
_c.text
|
|
996
|
+
]);
|
|
925
997
|
}
|
|
998
|
+
});
|
|
926
999
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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; });
|
|
1000
|
+
var _runPhase2 = function () {
|
|
1001
|
+
if (!_phase2Active) return;
|
|
1002
|
+
HistoryService.fetchHistory(_phase2Branch, _phase2Path).then(function (hist) {
|
|
1003
|
+
if (!_phase2Active) return;
|
|
1004
|
+
if (!hist || !hist.ops || hist.ops.length === 0) {
|
|
1005
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1006
|
+
return;
|
|
938
1007
|
}
|
|
939
|
-
} catch (e) {
|
|
940
|
-
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
941
|
-
_phase2Listener.dispose();
|
|
942
|
-
modelB.dispose();
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
946
1008
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
_phase2Listener.dispose();
|
|
950
|
-
modelB.dispose();
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
1009
|
+
var _lang = modelObj.getLanguageId();
|
|
1010
|
+
var modelB = window.monaco.editor.createModel(hist.base, _lang);
|
|
953
1011
|
|
|
954
|
-
|
|
1012
|
+
HistoryService.setReplayInProgress(_phase2Path, true);
|
|
955
1013
|
try {
|
|
956
|
-
for (var
|
|
957
|
-
var
|
|
1014
|
+
for (var _oi = 0; _oi < hist.ops.length; _oi++) {
|
|
1015
|
+
var _op = hist.ops[_oi];
|
|
958
1016
|
modelB.pushEditOperations([], [{
|
|
959
|
-
range: new window.monaco.Range(
|
|
960
|
-
text:
|
|
1017
|
+
range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
|
|
1018
|
+
text: _op[4] || ''
|
|
961
1019
|
}], function () { return null; });
|
|
962
1020
|
}
|
|
963
1021
|
} catch (e) {
|
|
964
|
-
|
|
1022
|
+
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
1023
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
965
1024
|
modelB.dispose();
|
|
966
1025
|
return;
|
|
967
1026
|
}
|
|
968
|
-
|
|
1027
|
+
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
969
1028
|
|
|
970
|
-
|
|
971
|
-
|
|
1029
|
+
// Fold in the edits made on model A while the replay was in flight,
|
|
1030
|
+
// THEN validate. Applying them after the check is what let a buffered
|
|
1031
|
+
// initial load slip through and duplicate the file (#92).
|
|
1032
|
+
if (_phase2Buf.length > 0) {
|
|
1033
|
+
try {
|
|
1034
|
+
for (var _bi = 0; _bi < _phase2Buf.length; _bi++) {
|
|
1035
|
+
var _bop = _phase2Buf[_bi];
|
|
1036
|
+
modelB.pushEditOperations([], [{
|
|
1037
|
+
range: new window.monaco.Range(_bop[0], _bop[1], _bop[2], _bop[3]),
|
|
1038
|
+
text: _bop[4] || ''
|
|
1039
|
+
}], function () { return null; });
|
|
1040
|
+
}
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1043
|
+
modelB.dispose();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
972
1047
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
}
|
|
1048
|
+
if (modelB.getValue() !== _phase2ModelA.getValue()) {
|
|
1049
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1050
|
+
modelB.dispose();
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1004
1053
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1054
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1055
|
+
if (!_phase2Active) { modelB.dispose(); return; }
|
|
1056
|
+
|
|
1057
|
+
if (modelB.getLanguageId() !== _lang) {
|
|
1058
|
+
window.monaco.editor.setModelLanguage(modelB, _lang);
|
|
1059
|
+
}
|
|
1060
|
+
modelB._mbeditorPath = _phase2Path;
|
|
1061
|
+
|
|
1062
|
+
var _vs = editor.saveViewState();
|
|
1063
|
+
editor.setModel(modelB);
|
|
1064
|
+
if (_vs) editor.restoreViewState(_vs);
|
|
1065
|
+
|
|
1066
|
+
var _oldEntry = window.__mbeditorModels[_phase2Path];
|
|
1067
|
+
if (_oldEntry && _oldEntry.model !== modelB) {
|
|
1068
|
+
var _oldModel = _oldEntry.model;
|
|
1069
|
+
// modelB holds the same text as modelA but its own version-id sequence,
|
|
1070
|
+
// so re-anchor the clean baseline. Carry the dirty state across: when the
|
|
1071
|
+
// tab was already dirty, -1 can never match a real AVI, so it stays dirty.
|
|
1072
|
+
var _oldClean = _oldEntry.cleanVersionId;
|
|
1073
|
+
var _wasDirty = _oldClean !== null && _oldClean !== undefined &&
|
|
1074
|
+
_oldModel.getAlternativeVersionId() !== _oldClean;
|
|
1075
|
+
var _newAvi = modelB.getAlternativeVersionId();
|
|
1076
|
+
window.__mbeditorModels[_phase2Path] = {
|
|
1077
|
+
model: modelB,
|
|
1078
|
+
aviBase: aviBaseRef.current,
|
|
1079
|
+
aviMax: _newAvi,
|
|
1080
|
+
lastAccessed: Date.now(),
|
|
1081
|
+
cleanVersionId: _wasDirty ? -1 : _newAvi
|
|
1082
|
+
};
|
|
1083
|
+
aviMaxRef.current = _newAvi;
|
|
1084
|
+
setTimeout(function () {
|
|
1085
|
+
if (_oldModel && !_oldModel.isDisposed()) _oldModel.dispose();
|
|
1086
|
+
}, 0);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Move the content listener onto modelB. It carries dirty tracking, content
|
|
1090
|
+
// sync and history recording — leaving it on modelA strands all three.
|
|
1091
|
+
contentDisposable.dispose();
|
|
1092
|
+
contentDisposable = _attachContentListener(modelB);
|
|
1093
|
+
}).catch(function () {
|
|
1094
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
1099
|
+
requestIdleCallback(_runPhase2, { timeout: 2000 });
|
|
1100
|
+
} else {
|
|
1101
|
+
setTimeout(_runPhase2, 200);
|
|
1102
|
+
}
|
|
1013
1103
|
};
|
|
1014
1104
|
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
1105
|
+
if (_contentLoaded) {
|
|
1106
|
+
_startHistory();
|
|
1017
1107
|
} else {
|
|
1018
|
-
|
|
1108
|
+
pendingHistorySetupRef.current = _startHistory;
|
|
1019
1109
|
}
|
|
1020
|
-
|
|
1021
|
-
_phase2CleanupFn = function () { _phase2Active = false; _phase2Listener.dispose(); };
|
|
1022
1110
|
}
|
|
1023
1111
|
|
|
1024
1112
|
return function () {
|
|
@@ -1055,15 +1143,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1055
1143
|
}
|
|
1056
1144
|
if (window.__mbeditorActiveEditor === editor) {
|
|
1057
1145
|
window.__mbeditorActiveEditor = null;
|
|
1146
|
+
window.dispatchEvent(new CustomEvent('mbeditor:active-editor'));
|
|
1058
1147
|
}
|
|
1148
|
+
if (scrollSyncDisposable) scrollSyncDisposable.dispose();
|
|
1149
|
+
if (_modelEntry.editor === editor) _modelEntry.editor = null;
|
|
1059
1150
|
if (editorPluginDisposable) editorPluginDisposable.dispose();
|
|
1060
1151
|
if (formatActionDisposable) formatActionDisposable.dispose();
|
|
1061
1152
|
runTestAtCursorDisposable.dispose();
|
|
1153
|
+
pastePlaceDisposable.dispose();
|
|
1062
1154
|
columnSelectDisposable.dispose();
|
|
1063
1155
|
contentDisposable.dispose();
|
|
1064
1156
|
EditorStore.setState({ canUndo: false, canRedo: false });
|
|
1157
|
+
pendingHistorySetupRef.current = null;
|
|
1065
1158
|
if (_phase2CleanupFn) _phase2CleanupFn();
|
|
1066
|
-
if (_collabActive) CollaborationService.unbindEditor(tab.path);
|
|
1159
|
+
if (_collabActive) CollaborationService.unbindEditor(tab.path, editor);
|
|
1067
1160
|
// Detach the model before disposing the editor so the model (and its undo
|
|
1068
1161
|
// history) survives for when the user returns to this tab.
|
|
1069
1162
|
editor.setModel(null);
|
|
@@ -1078,14 +1171,29 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1078
1171
|
var editor = monacoRef.current;
|
|
1079
1172
|
if (!editor || typeof tab.content !== 'string') return;
|
|
1080
1173
|
|
|
1174
|
+
// The editor effect defers persistent-undo tracking until the file load
|
|
1175
|
+
// lands (see pendingHistorySetupRef). Start it here, after the content has
|
|
1176
|
+
// been applied, so the loaded file is the history base rather than an op.
|
|
1177
|
+
var _runPendingHistory = function () {
|
|
1178
|
+
var pending = pendingHistorySetupRef.current;
|
|
1179
|
+
if (pending) {
|
|
1180
|
+
pendingHistorySetupRef.current = null;
|
|
1181
|
+
pending();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1081
1185
|
var extVersion = tab.externalContentVersion || 0;
|
|
1082
|
-
if (extVersion <= lastAppliedExternalVersionRef.current)
|
|
1186
|
+
if (extVersion <= lastAppliedExternalVersionRef.current) {
|
|
1187
|
+
_runPendingHistory();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1083
1190
|
|
|
1084
1191
|
// For a collaboration late-join the shared document is authoritative; applying
|
|
1085
1192
|
// the disk content would clobber it. Mark this version consumed and skip.
|
|
1086
1193
|
if (collabActiveRef.current && typeof CollaborationService !== 'undefined' &&
|
|
1087
1194
|
CollaborationService.consumesDiskLoad(tab.path)) {
|
|
1088
1195
|
lastAppliedExternalVersionRef.current = extVersion;
|
|
1196
|
+
_runPendingHistory();
|
|
1089
1197
|
return;
|
|
1090
1198
|
}
|
|
1091
1199
|
|
|
@@ -1098,39 +1206,48 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1098
1206
|
// Normalize before comparing to prevent false positive dirty edits
|
|
1099
1207
|
var vNorm = editor.getValue().replace(/\r\n/g, '\n');
|
|
1100
1208
|
var cNorm = tab.content.replace(/\r\n/g, '\n');
|
|
1101
|
-
if (vNorm
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1209
|
+
if (vNorm !== cNorm) {
|
|
1210
|
+
if (!vNorm) {
|
|
1211
|
+
// If the editor is currently completely empty, treat it as an initial load.
|
|
1212
|
+
// setValue clears the undo stack which is correct for initial load.
|
|
1213
|
+
// Null cleanVersionId before setValue so the synchronous onDidChangeContent
|
|
1214
|
+
// fires during setValue and skips the dirty check (cleanVersionId is null).
|
|
1215
|
+
var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
1216
|
+
if (_initEntry) _initEntry.cleanVersionId = null;
|
|
1217
|
+
|
|
1218
|
+
editor.setValue(tab.content);
|
|
1219
|
+
// Reset the AVI baseline: setValue clears the undo stack so anything before
|
|
1220
|
+
// this point is no longer reachable. Also clear the canUndo/canRedo display.
|
|
1221
|
+
var newBase = model.getAlternativeVersionId();
|
|
1222
|
+
aviBaseRef.current = newBase;
|
|
1223
|
+
aviMaxRef.current = newBase;
|
|
1224
|
+
if (_initEntry) _initEntry.cleanVersionId = newBase;
|
|
1225
|
+
EditorStore.setState({ canUndo: false, canRedo: false });
|
|
1226
|
+
} else {
|
|
1227
|
+
// Keep undo stack for formats or replaces by using executeEdits
|
|
1228
|
+
var _extEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
1229
|
+
if (_extEntry) _extEntry.cleanVersionId = null;
|
|
1230
|
+
editor.pushUndoStop();
|
|
1231
|
+
editor.executeEdits("external", [{
|
|
1232
|
+
range: model.getFullModelRange(),
|
|
1233
|
+
text: tab.content
|
|
1234
|
+
}]);
|
|
1235
|
+
editor.pushUndoStop();
|
|
1236
|
+
// Re-anchor the clean baseline so onDidChangeContent doesn't mark this
|
|
1237
|
+
// externally-applied content as a dirty edit.
|
|
1238
|
+
var _newExtAvi = model.getAlternativeVersionId();
|
|
1239
|
+
if (_extEntry) _extEntry.cleanVersionId = _newExtAvi;
|
|
1240
|
+
aviBaseRef.current = _newExtAvi;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Empty file: content already matches, but tracking still has to start.
|
|
1245
|
+
_runPendingHistory();
|
|
1246
|
+
|
|
1247
|
+
// The model now holds the disk content. A room that deferred attaching while
|
|
1248
|
+
// this fetch was in flight can seed the shared doc from it.
|
|
1249
|
+
if (collabActiveRef.current && typeof CollaborationService !== 'undefined') {
|
|
1250
|
+
CollaborationService.contentReady(tab.path);
|
|
1134
1251
|
}
|
|
1135
1252
|
}, [tab.content, tab.externalContentVersion]);
|
|
1136
1253
|
|
|
@@ -1216,7 +1333,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1216
1333
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1217
1334
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
1218
1335
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
1219
|
-
linkedEditing:
|
|
1336
|
+
linkedEditing: true,
|
|
1220
1337
|
acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on'
|
|
1221
1338
|
});
|
|
1222
1339
|
// Re-apply large-file tuning last so a prefs change can't re-enable the
|
|
@@ -1694,9 +1811,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1694
1811
|
// keystroke. This ticks 250 ms after the last change instead — the same
|
|
1695
1812
|
// treatment the route-hint redraw above already gets.
|
|
1696
1813
|
var _useStateZT = useState(0);
|
|
1697
|
-
var
|
|
1698
|
-
var
|
|
1699
|
-
var setZoneTick = _useStateZT2[1];
|
|
1814
|
+
var zoneTick = _useStateZT[0];
|
|
1815
|
+
var setZoneTick = _useStateZT[1];
|
|
1700
1816
|
useEffect(function () {
|
|
1701
1817
|
var timer = setTimeout(function () {
|
|
1702
1818
|
setZoneTick(function (n) { return n + 1; });
|
|
@@ -1966,33 +2082,71 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1966
2082
|
if (isMarkdown && window.marked) {
|
|
1967
2083
|
(function () {
|
|
1968
2084
|
var renderer = new window.marked.Renderer();
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2085
|
+
// escapeHtml and safeHref are defined once in ChangelogView.js — the
|
|
2086
|
+
// href allow-list guards both markdown surfaces and must not fork.
|
|
2087
|
+
var escapeHtml = window.mbeditorEscapeHtml;
|
|
2088
|
+
var safeHref = window.mbeditorSafeHref;
|
|
1972
2089
|
renderer.html = function (token) {
|
|
1973
2090
|
return '<pre>' + escapeHtml(typeof token === 'object' ? token.text : token) + '</pre>';
|
|
1974
2091
|
};
|
|
1975
|
-
// Sanitize link/image hrefs to block javascript: and data: schemes.
|
|
1976
2092
|
var _origLink = renderer.link.bind(renderer);
|
|
1977
2093
|
var _origImage = renderer.image.bind(renderer);
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2094
|
+
// marked 10 calls these positionally — `.link(href, title, text)`, see
|
|
2095
|
+
// the vendored bundle. An earlier version took a token object, and
|
|
2096
|
+
// against the real signature the object branch never ran: safeHref was
|
|
2097
|
+
// dead, so javascript: hrefs passed through, and dropping title/text
|
|
2098
|
+
// rendered every link as "undefined".
|
|
2099
|
+
renderer.link = function(href, title, text) {
|
|
2100
|
+
return _origLink(safeHref(href), title, text);
|
|
1982
2101
|
};
|
|
1983
|
-
renderer.
|
|
1984
|
-
|
|
1985
|
-
return _origLink(token);
|
|
2102
|
+
renderer.image = function(href, title, text) {
|
|
2103
|
+
return _origImage(safeHref(href), title, text);
|
|
1986
2104
|
};
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2105
|
+
// marked 10 dropped heading ids, so every in-document `[x](#anchor)`
|
|
2106
|
+
// link pointed at nothing. Slugs follow GitHub's: strip markup, lower,
|
|
2107
|
+
// drop punctuation, spaces to hyphens, `-1`/`-2` for repeats.
|
|
2108
|
+
var slugSeen = {};
|
|
2109
|
+
renderer.heading = function (text, level) {
|
|
2110
|
+
var slug = String(text)
|
|
2111
|
+
.replace(/<[^>]+>/g, '')
|
|
2112
|
+
.replace(/&[#\w]+;/g, '')
|
|
2113
|
+
.trim().toLowerCase()
|
|
2114
|
+
.replace(/[^\w\- ]+/g, '')
|
|
2115
|
+
.replace(/\s/g, '-');
|
|
2116
|
+
slugSeen[slug] = (slugSeen[slug] || 0) + 1;
|
|
2117
|
+
if (slugSeen[slug] > 1) slug += '-' + (slugSeen[slug] - 1);
|
|
2118
|
+
return '<h' + level + ' id="' + slug + '">' + text + '</h' + level + '>\n';
|
|
1990
2119
|
};
|
|
1991
2120
|
setMarkup(window.marked.parse(markdownContent, { renderer: renderer }));
|
|
1992
2121
|
})();
|
|
1993
2122
|
}
|
|
1994
2123
|
}, [markdownContent, isMarkdown]);
|
|
1995
2124
|
|
|
2125
|
+
// Preview -> source scroll sync. The source editor instance (a different
|
|
2126
|
+
// EditorPanel, possibly a different pane) is published on the shared model
|
|
2127
|
+
// entry by the editor-creation effect above; scrollSyncing on that same
|
|
2128
|
+
// entry is the shared flag that stops the two directions ping-ponging.
|
|
2129
|
+
useEffect(function () {
|
|
2130
|
+
if (!tab.isPreview || !isMarkdown) return;
|
|
2131
|
+
var el = previewScrollRef.current;
|
|
2132
|
+
if (!el) return;
|
|
2133
|
+
function onScroll() {
|
|
2134
|
+
var modelEntry = window.__mbeditorModels && window.__mbeditorModels[sourcePath];
|
|
2135
|
+
var editor = modelEntry && modelEntry.editor;
|
|
2136
|
+
if (!editor || (modelEntry && modelEntry.scrollSyncing)) return;
|
|
2137
|
+
var max = el.scrollHeight - el.clientHeight;
|
|
2138
|
+
if (max <= 0) return;
|
|
2139
|
+
var fraction = el.scrollTop / max;
|
|
2140
|
+
var scrollHeight = editor.getScrollHeight() - editor.getLayoutInfo().height;
|
|
2141
|
+
if (scrollHeight <= 0) return;
|
|
2142
|
+
modelEntry.scrollSyncing = true;
|
|
2143
|
+
editor.setScrollTop(fraction * scrollHeight);
|
|
2144
|
+
modelEntry.scrollSyncing = false;
|
|
2145
|
+
}
|
|
2146
|
+
el.addEventListener('scroll', onScroll);
|
|
2147
|
+
return function () { el.removeEventListener('scroll', onScroll); };
|
|
2148
|
+
}, [tab.isPreview, isMarkdown, sourcePath]);
|
|
2149
|
+
|
|
1996
2150
|
// Click-outside handler to close the methods dropdown
|
|
1997
2151
|
useEffect(function() {
|
|
1998
2152
|
if (!methodsOpen) return;
|
|
@@ -2041,7 +2195,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2041
2195
|
// our own, which makes it async — the worker is where the parse already
|
|
2042
2196
|
// lives, and it tracks the live buffer, so unsaved edits are included.
|
|
2043
2197
|
function loadJsOutline(model) {
|
|
2044
|
-
var ts = window.monaco && window.monaco.
|
|
2198
|
+
var ts = window.monaco && window.monaco.typescript;
|
|
2045
2199
|
if (!ts || !window.JsOutline) return Promise.reject(new Error('outline unavailable'));
|
|
2046
2200
|
|
|
2047
2201
|
var workerFor = model.getLanguageId() === 'typescript' ? ts.getTypeScriptWorker : ts.getJavaScriptWorker;
|
|
@@ -2102,7 +2256,12 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2102
2256
|
}
|
|
2103
2257
|
|
|
2104
2258
|
if (tab.isPreview && isMarkdown) {
|
|
2105
|
-
return React.createElement('div', {
|
|
2259
|
+
return React.createElement('div', {
|
|
2260
|
+
className: 'markdown-preview markdown-preview-full',
|
|
2261
|
+
ref: previewScrollRef,
|
|
2262
|
+
'data-preview-for': sourcePath,
|
|
2263
|
+
dangerouslySetInnerHTML: { __html: markup }
|
|
2264
|
+
});
|
|
2106
2265
|
}
|
|
2107
2266
|
|
|
2108
2267
|
// Helper: shorten long paths by showing the last 2 segments with a leading ellipsis
|
|
@@ -2113,6 +2272,38 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2113
2272
|
return '\u2026/' + parts.slice(-2).join('/');
|
|
2114
2273
|
}
|
|
2115
2274
|
|
|
2275
|
+
// Breadcrumb: shortPath()'s segments, chevron-separated, the final one an
|
|
2276
|
+
// icon + filename in the normal text colour, everything before it muted \u2014
|
|
2277
|
+
// VS Code's editor breadcrumb. Look only; not a navigation control.
|
|
2278
|
+
function renderBreadcrumb(path) {
|
|
2279
|
+
var segments = shortPath(path).split('/');
|
|
2280
|
+
var last = segments.length - 1;
|
|
2281
|
+
var nodes = [];
|
|
2282
|
+
segments.forEach(function (seg, i) {
|
|
2283
|
+
if (i > 0) {
|
|
2284
|
+
nodes.push(React.createElement('i', {
|
|
2285
|
+
key: 'sep-' + i,
|
|
2286
|
+
className: 'fas fa-chevron-right ide-breadcrumb-sep',
|
|
2287
|
+
'aria-hidden': 'true'
|
|
2288
|
+
}));
|
|
2289
|
+
}
|
|
2290
|
+
if (i === last) {
|
|
2291
|
+
nodes.push(React.createElement(
|
|
2292
|
+
'span',
|
|
2293
|
+
{ key: 'seg-' + i, className: 'ide-breadcrumb-file' },
|
|
2294
|
+
React.createElement('i', {
|
|
2295
|
+
className: (window.getFileIcon ? window.getFileIcon(path) : 'far fa-file-code') + ' ide-breadcrumb-file-icon',
|
|
2296
|
+
'aria-hidden': 'true'
|
|
2297
|
+
}),
|
|
2298
|
+
seg
|
|
2299
|
+
));
|
|
2300
|
+
} else {
|
|
2301
|
+
nodes.push(React.createElement('span', { key: 'seg-' + i, className: 'ide-breadcrumb-dir' }, seg));
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
return nodes;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2116
2307
|
// While Monaco is still loading, show a lightweight skeleton so the UI is
|
|
2117
2308
|
// visible immediately without calling monaco.editor.create() too early.
|
|
2118
2309
|
if (!monacoReady) {
|
|
@@ -2191,7 +2382,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2191
2382
|
React.createElement(
|
|
2192
2383
|
'span',
|
|
2193
2384
|
{ className: 'ide-editor-file-location', title: tab.path },
|
|
2194
|
-
|
|
2385
|
+
renderBreadcrumb(tab.path)
|
|
2195
2386
|
),
|
|
2196
2387
|
gitAvailable && tab.path && React.createElement(
|
|
2197
2388
|
'button',
|
|
@@ -2200,8 +2391,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2200
2391
|
onClick: function() { if (onShowHistory) onShowHistory(tab.path); },
|
|
2201
2392
|
title: 'File History'
|
|
2202
2393
|
},
|
|
2203
|
-
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.
|
|
2204
|
-
|
|
2394
|
+
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
|
|
2395
|
+
editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
|
|
2205
2396
|
),
|
|
2206
2397
|
hasOutline && React.createElement(
|
|
2207
2398
|
'button',
|
|
@@ -2255,8 +2446,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2255
2446
|
},
|
|
2256
2447
|
title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
|
|
2257
2448
|
},
|
|
2258
|
-
React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.
|
|
2259
|
-
|
|
2449
|
+
React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
|
|
2450
|
+
editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, isTestOutline ? 'Outline' : 'Methods')
|
|
2260
2451
|
),
|
|
2261
2452
|
gitAvailable && React.createElement(
|
|
2262
2453
|
'button',
|
|
@@ -2265,8 +2456,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2265
2456
|
onClick: function() { setIsBlameVisible(function(prev) { return !prev; }); },
|
|
2266
2457
|
title: 'Toggle Git Blame'
|
|
2267
2458
|
},
|
|
2268
|
-
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.
|
|
2269
|
-
|
|
2459
|
+
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarLabels ? '5px' : 0, flexShrink: 0 } }),
|
|
2460
|
+
editorPrefs.toolbarLabels && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
|
|
2270
2461
|
),
|
|
2271
2462
|
),
|
|
2272
2463
|
conflictCount > 0 && React.createElement(
|
|
@@ -2386,7 +2577,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2386
2577
|
React.createElement(
|
|
2387
2578
|
'span',
|
|
2388
2579
|
{ style: { flex: 1, textAlign: 'center' } },
|
|
2389
|
-
'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' +
|
|
2580
|
+
'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + formatSize(pageTotalBytes) + ')'
|
|
2390
2581
|
),
|
|
2391
2582
|
React.createElement(
|
|
2392
2583
|
'button',
|
|
@@ -2428,7 +2619,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2428
2619
|
)
|
|
2429
2620
|
),
|
|
2430
2621
|
React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
|
|
2431
|
-
|
|
2622
|
+
// Portalled to body: under the glass chrome the centre column has a
|
|
2623
|
+
// backdrop-filter, which makes it the containing block for position:fixed
|
|
2624
|
+
// and shoves the menu ~130px off its button.
|
|
2625
|
+
methodsOpen && methodsDropdownPos && ReactDOM.createPortal(React.createElement(
|
|
2432
2626
|
'div',
|
|
2433
2627
|
{
|
|
2434
2628
|
ref: methodsDropdownRef,
|
|
@@ -2526,7 +2720,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2526
2720
|
}
|
|
2527
2721
|
return rows;
|
|
2528
2722
|
})()
|
|
2529
|
-
),
|
|
2723
|
+
), document.body),
|
|
2530
2724
|
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
2725
|
);
|
|
2532
2726
|
};
|