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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -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/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +429 -247
- 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/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- 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 +2 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +38 -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 +642 -248
- data/app/assets/stylesheets/mbeditor/glass.css +163 -0
- data/app/assets/stylesheets/mbeditor/themes.css +90 -30
- 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 +117 -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/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 +15 -9
- data/lib/mbeditor/route_map.rb +4 -0
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- 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
|
|
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 });
|
|
@@ -768,14 +806,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
768
806
|
EditorStore.setState({ isQuickOpenVisible: true });
|
|
769
807
|
});
|
|
770
808
|
|
|
771
|
-
// PageUp/PageDown
|
|
772
|
-
//
|
|
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.
|
|
812
|
+
TabManager.navigateBack();
|
|
776
813
|
});
|
|
777
814
|
editor.addCommand(window.monaco.KeyCode.PageDown, function() {
|
|
778
|
-
TabManager.
|
|
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
|
-
|
|
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,
|
|
926
|
+
TabManager.markDirty(paneId, tab.id, _contentProvider);
|
|
874
927
|
} else {
|
|
875
|
-
TabManager.markClean(paneId, tab.id,
|
|
928
|
+
TabManager.markClean(paneId, tab.id, _contentProvider);
|
|
876
929
|
}
|
|
877
930
|
}
|
|
878
931
|
|
|
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);
|
|
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
|
|
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 (
|
|
899
|
-
var
|
|
900
|
-
var
|
|
901
|
-
var
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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; });
|
|
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
|
-
|
|
948
|
-
|
|
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
|
-
|
|
1001
|
+
HistoryService.setReplayInProgress(_phase2Path, true);
|
|
955
1002
|
try {
|
|
956
|
-
for (var
|
|
957
|
-
var
|
|
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(
|
|
960
|
-
text:
|
|
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
|
-
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1037
|
+
if (modelB.getValue() !== _phase2ModelA.getValue()) {
|
|
1038
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1039
|
+
modelB.dispose();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1004
1042
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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 (
|
|
1016
|
-
|
|
1094
|
+
if (_contentLoaded) {
|
|
1095
|
+
_startHistory();
|
|
1017
1096
|
} else {
|
|
1018
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
1698
|
-
var
|
|
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
|
-
|
|
1970
|
-
|
|
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
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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.
|
|
1984
|
-
|
|
1985
|
-
return _origLink(token);
|
|
2090
|
+
renderer.image = function(href, title, text) {
|
|
2091
|
+
return _origImage(safeHref(href), title, text);
|
|
1986
2092
|
};
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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', {
|
|
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
|
-
|
|
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.
|
|
2204
|
-
|
|
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.
|
|
2259
|
-
|
|
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.
|
|
2269
|
-
|
|
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 + ' (' +
|
|
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
|
-
|
|
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
|
};
|