mbeditor 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +157 -1
- data/app/assets/javascripts/mbeditor/application.js +3 -1
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
- 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 +440 -334
- 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/ImportDialog.js +9 -1
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
- 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 +694 -306
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +46 -57
- 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 +29 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
- 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 +762 -297
- 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 +25 -6
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +125 -465
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- 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_globals_service.rb +12 -1
- 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/rubocop_run_service.rb +17 -5
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/schema_service.rb +8 -2
- data/app/services/mbeditor/search_replace_service.rb +19 -2
- data/app/services/mbeditor/test_runner_service.rb +3 -56
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -9
- data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
- data/lib/mbeditor/route_map.rb +4 -1
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +14 -3
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
|
@@ -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;
|
|
@@ -24,16 +23,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
24
23
|
var onContentChange = _ref.onContentChange;
|
|
25
24
|
var markers = _ref.markers;
|
|
26
25
|
var gitAvailable = _ref.gitAvailable === true;
|
|
27
|
-
var testAvailable = _ref.testAvailable === true;
|
|
28
26
|
var onFormat = _ref.onFormat;
|
|
29
27
|
var onSave = _ref.onSave;
|
|
30
|
-
var onRunTest = _ref.onRunTest;
|
|
31
28
|
var onRunTestAtCursor = _ref.onRunTestAtCursor;
|
|
32
29
|
var onShowHistory = _ref.onShowHistory;
|
|
33
30
|
var treeData = _ref.treeData || [];
|
|
34
31
|
var testResult = _ref.testResult;
|
|
35
32
|
var testPanelFile = _ref.testPanelFile;
|
|
36
|
-
var testLoading = _ref.testLoading;
|
|
37
33
|
var testInlineVisible = _ref.testInlineVisible;
|
|
38
34
|
var editorPrefs = _ref.editorPrefs || {};
|
|
39
35
|
var monacoReady = _ref.monacoReady !== false; // undefined means Monaco already loaded (legacy callers)
|
|
@@ -42,6 +38,11 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
42
38
|
var monacoRef = useRef(null);
|
|
43
39
|
var latestContentRef = useRef('');
|
|
44
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);
|
|
45
46
|
var conflictDecorationsRef = React.useRef([]);
|
|
46
47
|
var conflictBlocksRef = React.useRef([]);
|
|
47
48
|
var aviBaseRef = useRef(0);
|
|
@@ -70,24 +71,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
70
71
|
var currentConflictIndexRef = React.useRef(0);
|
|
71
72
|
|
|
72
73
|
var _useState = useState('');
|
|
73
|
-
var
|
|
74
|
-
var
|
|
75
|
-
var setMarkup = _useState2[1];
|
|
74
|
+
var markup = _useState[0];
|
|
75
|
+
var setMarkup = _useState[1];
|
|
76
76
|
|
|
77
77
|
var _useState3 = useState(false);
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
var setIsBlameVisible = _useState4[1];
|
|
78
|
+
var isBlameVisible = _useState3[0];
|
|
79
|
+
var setIsBlameVisible = _useState3[1];
|
|
81
80
|
|
|
82
81
|
var _useState5 = useState(null);
|
|
83
|
-
var
|
|
84
|
-
var
|
|
85
|
-
var setBlameData = _useState6[1];
|
|
82
|
+
var blameData = _useState5[0];
|
|
83
|
+
var setBlameData = _useState5[1];
|
|
86
84
|
|
|
87
85
|
var _useState7 = useState(false);
|
|
88
|
-
var
|
|
89
|
-
var
|
|
90
|
-
var setIsBlameLoading = _useState8[1];
|
|
86
|
+
var isBlameLoading = _useState7[0];
|
|
87
|
+
var setIsBlameLoading = _useState7[1];
|
|
91
88
|
|
|
92
89
|
var blameDecorationsRef = useRef([]);
|
|
93
90
|
var gitLineDecorationsRef = useRef([]);
|
|
@@ -100,34 +97,28 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
100
97
|
var testZoneIdsRef = useRef([]);
|
|
101
98
|
|
|
102
99
|
var _useState9 = useState(false);
|
|
103
|
-
var
|
|
104
|
-
var
|
|
105
|
-
var setEditorReady = _useState10[1];
|
|
100
|
+
var editorReady = _useState9[0];
|
|
101
|
+
var setEditorReady = _useState9[1];
|
|
106
102
|
|
|
107
103
|
var _useState11 = useState(false);
|
|
108
|
-
var
|
|
109
|
-
var
|
|
110
|
-
var setMethodsOpen = _useState12[1];
|
|
104
|
+
var methodsOpen = _useState11[0];
|
|
105
|
+
var setMethodsOpen = _useState11[1];
|
|
111
106
|
|
|
112
107
|
var _useState13 = useState([]);
|
|
113
|
-
var
|
|
114
|
-
var
|
|
115
|
-
var setMethodsList = _useState14[1];
|
|
108
|
+
var methodsList = _useState13[0];
|
|
109
|
+
var setMethodsList = _useState13[1];
|
|
116
110
|
|
|
117
111
|
var _useState15 = useState(false);
|
|
118
|
-
var
|
|
119
|
-
var
|
|
120
|
-
var setMethodsTruncated = _useState16[1];
|
|
112
|
+
var methodsTruncated = _useState15[0];
|
|
113
|
+
var setMethodsTruncated = _useState15[1];
|
|
121
114
|
|
|
122
115
|
var _useState17 = useState(false);
|
|
123
|
-
var
|
|
124
|
-
var
|
|
125
|
-
var setMethodsUnavailable = _useState18[1];
|
|
116
|
+
var methodsUnavailable = _useState17[0];
|
|
117
|
+
var setMethodsUnavailable = _useState17[1];
|
|
126
118
|
|
|
127
119
|
var _useState19 = useState(null);
|
|
128
|
-
var
|
|
129
|
-
var
|
|
130
|
-
var setMethodsDropdownPos = _useState20[1];
|
|
120
|
+
var methodsDropdownPos = _useState19[0];
|
|
121
|
+
var setMethodsDropdownPos = _useState19[1];
|
|
131
122
|
|
|
132
123
|
var methodsBtnRef = useRef(null);
|
|
133
124
|
var methodsDropdownRef = useRef(null);
|
|
@@ -136,24 +127,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
136
127
|
|
|
137
128
|
// Local pagination state — initialized from tab props; updated on page navigation
|
|
138
129
|
var _useState21 = useState(tab.startLine || 0);
|
|
139
|
-
var
|
|
140
|
-
var
|
|
141
|
-
var setPageStartLine = _useState22[1];
|
|
130
|
+
var pageStartLine = _useState21[0];
|
|
131
|
+
var setPageStartLine = _useState21[1];
|
|
142
132
|
|
|
143
133
|
var _useState23 = useState(tab.lineCount || 0);
|
|
144
|
-
var
|
|
145
|
-
var
|
|
146
|
-
var setPageLineCount = _useState24[1];
|
|
134
|
+
var pageLineCount = _useState23[0];
|
|
135
|
+
var setPageLineCount = _useState23[1];
|
|
147
136
|
|
|
148
137
|
var _useState25 = useState(tab.totalLines || 0);
|
|
149
|
-
var
|
|
150
|
-
var
|
|
151
|
-
var setPageTotalLines = _useState26[1];
|
|
138
|
+
var pageTotalLines = _useState25[0];
|
|
139
|
+
var setPageTotalLines = _useState25[1];
|
|
152
140
|
|
|
153
141
|
var _useState27 = useState(tab.totalBytes || 0);
|
|
154
|
-
var
|
|
155
|
-
var
|
|
156
|
-
var setPageTotalBytes = _useState28[1];
|
|
142
|
+
var pageTotalBytes = _useState27[0];
|
|
143
|
+
var setPageTotalBytes = _useState27[1];
|
|
157
144
|
|
|
158
145
|
var onFormatRef = useRef(onFormat);
|
|
159
146
|
onFormatRef.current = onFormat;
|
|
@@ -168,12 +155,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
168
155
|
|
|
169
156
|
var vimStatusRef = useRef(null);
|
|
170
157
|
var vimModeObjRef = useRef(null);
|
|
171
|
-
|
|
172
|
-
function humanSize(bytes) {
|
|
173
|
-
if (bytes < 1024) return bytes + ' B';
|
|
174
|
-
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
175
|
-
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
176
|
-
}
|
|
158
|
+
var previewScrollRef = useRef(null);
|
|
177
159
|
|
|
178
160
|
var clearTestZones = function clearTestZones(editor) {
|
|
179
161
|
if (!editor) return;
|
|
@@ -518,6 +500,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
518
500
|
language = 'javascript';
|
|
519
501
|
} else if (/\.css\.erb$/.test(fileNameLower)) {
|
|
520
502
|
language = 'css';
|
|
503
|
+
} else if (/\.scss\.erb$/.test(fileNameLower)) {
|
|
504
|
+
language = 'scss';
|
|
505
|
+
} else if (/\.less\.erb$/.test(fileNameLower)) {
|
|
506
|
+
language = 'less';
|
|
521
507
|
} else if (/\.html\.erb$/.test(fileNameLower)) {
|
|
522
508
|
language = 'erb';
|
|
523
509
|
} else if (/\.html\.haml$/.test(fileNameLower)) {
|
|
@@ -543,8 +529,16 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
543
529
|
language = 'javascript';break;
|
|
544
530
|
case 'ts':case 'tsx':
|
|
545
531
|
language = 'typescript';break;
|
|
546
|
-
|
|
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':
|
|
547
537
|
language = 'css';break;
|
|
538
|
+
case 'scss':
|
|
539
|
+
language = 'scss';break;
|
|
540
|
+
case 'less':
|
|
541
|
+
language = 'less';break;
|
|
548
542
|
case 'html':
|
|
549
543
|
language = 'html';break;
|
|
550
544
|
case 'erb':
|
|
@@ -607,16 +601,26 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
607
601
|
// untitled://, mbeditor:// and friends have no file to persist history
|
|
608
602
|
// against — tracking them just produces doomed /file_history requests.
|
|
609
603
|
var _virtualPath = (tab.path || '').indexOf('://') >= 0;
|
|
610
|
-
|
|
611
|
-
|
|
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) {
|
|
612
613
|
var _histBranch = EditorStore.getState().gitBranch || '';
|
|
613
614
|
if (_histBranch) {
|
|
614
|
-
|
|
615
|
-
HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
|
|
616
|
-
} else {
|
|
617
|
-
HistoryService.beginTracking(_histBranch, tab.path, tab.content || '');
|
|
618
|
-
}
|
|
615
|
+
HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
|
|
619
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
|
+
};
|
|
620
624
|
}
|
|
621
625
|
|
|
622
626
|
// Sync latestContentRef from the actual model content so the onDidChangeContent
|
|
@@ -655,15 +659,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
655
659
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
656
660
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
657
661
|
acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on',
|
|
658
|
-
linkedEditing:
|
|
662
|
+
linkedEditing: true,
|
|
659
663
|
fixedOverflowWidgets: true,
|
|
660
664
|
hover: { above: false }
|
|
661
665
|
});
|
|
662
666
|
|
|
663
667
|
applyLargeFileTuning(editor, modelObj);
|
|
664
668
|
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
667
676
|
}
|
|
668
677
|
|
|
669
678
|
// Restore the find widget state from the previous editor (when persistFindState is on).
|
|
@@ -708,6 +717,32 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
708
717
|
|
|
709
718
|
monacoRef.current = editor;
|
|
710
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'));
|
|
711
746
|
// Apply read-only for paginated (truncated) files
|
|
712
747
|
if (tab.truncated) {
|
|
713
748
|
editor.updateOptions({ readOnly: true });
|
|
@@ -771,14 +806,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
771
806
|
EditorStore.setState({ isQuickOpenVisible: true });
|
|
772
807
|
});
|
|
773
808
|
|
|
774
|
-
// PageUp/PageDown
|
|
775
|
-
//
|
|
776
|
-
// 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.
|
|
777
811
|
editor.addCommand(window.monaco.KeyCode.PageUp, function() {
|
|
778
|
-
TabManager.
|
|
812
|
+
TabManager.navigateBack();
|
|
779
813
|
});
|
|
780
814
|
editor.addCommand(window.monaco.KeyCode.PageDown, function() {
|
|
781
|
-
TabManager.
|
|
815
|
+
TabManager.navigateForward();
|
|
782
816
|
});
|
|
783
817
|
|
|
784
818
|
var editorPluginDisposable = null;
|
|
@@ -846,7 +880,25 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
846
880
|
// below can swap the model, so this must be re-attached to the replacement —
|
|
847
881
|
// a listener left on the old model would silently stop tracking edits.
|
|
848
882
|
var _attachContentListener = function (model) {
|
|
849
|
-
|
|
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) {
|
|
850
902
|
if (!_collabActive && typeof HistoryService !== 'undefined') {
|
|
851
903
|
HistoryService.recordOps(tab.path, e.changes);
|
|
852
904
|
}
|
|
@@ -864,8 +916,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
864
916
|
EditorStore.setState({ canUndo: newCanUndo, canRedo: newCanRedo });
|
|
865
917
|
}
|
|
866
918
|
|
|
867
|
-
var val = model.getValue();
|
|
868
|
-
|
|
869
919
|
// Dirty-state tracking via alternativeVersionId — O(1), no string comparison.
|
|
870
920
|
// AVI decrements on undo so it returns to cleanVersionId after a full undo.
|
|
871
921
|
// Skip entirely when cleanVersionId is null — file is mid-load, not yet settled.
|
|
@@ -873,155 +923,179 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
873
923
|
var _cleanAvi = _entry && _entry.cleanVersionId;
|
|
874
924
|
if (_cleanAvi !== null && _cleanAvi !== undefined) {
|
|
875
925
|
if (currentAvi !== _cleanAvi) {
|
|
876
|
-
TabManager.markDirty(paneId, tab.id,
|
|
926
|
+
TabManager.markDirty(paneId, tab.id, _contentProvider);
|
|
877
927
|
} else {
|
|
878
|
-
TabManager.markClean(paneId, tab.id,
|
|
928
|
+
TabManager.markClean(paneId, tab.id, _contentProvider);
|
|
879
929
|
}
|
|
880
930
|
}
|
|
881
931
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
var cNorm = currentContent.replace(/\r\n/g, '\n');
|
|
887
|
-
if (vNorm !== cNorm) {
|
|
888
|
-
// Update the ref immediately so rapid undo/redo events compare against the
|
|
889
|
-
// latest content rather than a stale snapshot from a previous React render.
|
|
890
|
-
latestContentRef.current = val;
|
|
891
|
-
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);
|
|
892
936
|
}
|
|
893
937
|
});
|
|
938
|
+
return {
|
|
939
|
+
dispose: function () {
|
|
940
|
+
if (_contentSyncTimer !== null) { clearTimeout(_contentSyncTimer); _contentSyncTimer = null; }
|
|
941
|
+
_contentListener.dispose();
|
|
942
|
+
}
|
|
943
|
+
};
|
|
894
944
|
};
|
|
895
945
|
|
|
896
946
|
var contentDisposable = _attachContentListener(modelObj);
|
|
897
947
|
|
|
898
948
|
// Phase 2: background undo-history replay.
|
|
899
|
-
// 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.
|
|
900
952
|
var _phase2CleanupFn = null;
|
|
901
|
-
if (
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
var
|
|
905
|
-
var
|
|
906
|
-
var
|
|
907
|
-
|
|
908
|
-
var
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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;
|
|
972
|
+
|
|
973
|
+
HistoryService.beginTracking(_phase2Branch, _phase2Path, _phase2ModelA.getValue());
|
|
920
974
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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) {
|
|
924
978
|
if (!_phase2Active) return;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
]);
|
|
928
986
|
}
|
|
987
|
+
});
|
|
929
988
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
modelB.pushEditOperations([], [{
|
|
938
|
-
range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
|
|
939
|
-
text: _op[4] || ''
|
|
940
|
-
}], 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;
|
|
941
996
|
}
|
|
942
|
-
} catch (e) {
|
|
943
|
-
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
944
|
-
_phase2Listener.dispose();
|
|
945
|
-
modelB.dispose();
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
949
997
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
_phase2Listener.dispose();
|
|
953
|
-
modelB.dispose();
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
998
|
+
var _lang = modelObj.getLanguageId();
|
|
999
|
+
var modelB = window.monaco.editor.createModel(hist.base, _lang);
|
|
956
1000
|
|
|
957
|
-
|
|
1001
|
+
HistoryService.setReplayInProgress(_phase2Path, true);
|
|
958
1002
|
try {
|
|
959
|
-
for (var
|
|
960
|
-
var
|
|
1003
|
+
for (var _oi = 0; _oi < hist.ops.length; _oi++) {
|
|
1004
|
+
var _op = hist.ops[_oi];
|
|
961
1005
|
modelB.pushEditOperations([], [{
|
|
962
|
-
range: new window.monaco.Range(
|
|
963
|
-
text:
|
|
1006
|
+
range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
|
|
1007
|
+
text: _op[4] || ''
|
|
964
1008
|
}], function () { return null; });
|
|
965
1009
|
}
|
|
966
1010
|
} catch (e) {
|
|
967
|
-
|
|
1011
|
+
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
1012
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
968
1013
|
modelB.dispose();
|
|
969
1014
|
return;
|
|
970
1015
|
}
|
|
971
|
-
|
|
1016
|
+
HistoryService.setReplayInProgress(_phase2Path, false);
|
|
972
1017
|
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
}
|
|
975
1036
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
var _vs = editor.saveViewState();
|
|
982
|
-
editor.setModel(modelB);
|
|
983
|
-
if (_vs) editor.restoreViewState(_vs);
|
|
984
|
-
|
|
985
|
-
var _oldEntry = window.__mbeditorModels[_phase2Path];
|
|
986
|
-
if (_oldEntry && _oldEntry.model !== modelB) {
|
|
987
|
-
var _oldModel = _oldEntry.model;
|
|
988
|
-
// modelB holds the same text as modelA but its own version-id sequence,
|
|
989
|
-
// so re-anchor the clean baseline. Carry the dirty state across: when the
|
|
990
|
-
// tab was already dirty, -1 can never match a real AVI, so it stays dirty.
|
|
991
|
-
var _oldClean = _oldEntry.cleanVersionId;
|
|
992
|
-
var _wasDirty = _oldClean !== null && _oldClean !== undefined &&
|
|
993
|
-
_oldModel.getAlternativeVersionId() !== _oldClean;
|
|
994
|
-
var _newAvi = modelB.getAlternativeVersionId();
|
|
995
|
-
window.__mbeditorModels[_phase2Path] = {
|
|
996
|
-
model: modelB,
|
|
997
|
-
aviBase: aviBaseRef.current,
|
|
998
|
-
aviMax: _newAvi,
|
|
999
|
-
lastAccessed: Date.now(),
|
|
1000
|
-
cleanVersionId: _wasDirty ? -1 : _newAvi
|
|
1001
|
-
};
|
|
1002
|
-
aviMaxRef.current = _newAvi;
|
|
1003
|
-
setTimeout(function () {
|
|
1004
|
-
if (_oldModel && !_oldModel.isDisposed()) _oldModel.dispose();
|
|
1005
|
-
}, 0);
|
|
1006
|
-
}
|
|
1037
|
+
if (modelB.getValue() !== _phase2ModelA.getValue()) {
|
|
1038
|
+
if (_phase2Listener) _phase2Listener.dispose();
|
|
1039
|
+
modelB.dispose();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1007
1042
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
+
}
|
|
1016
1092
|
};
|
|
1017
1093
|
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1094
|
+
if (_contentLoaded) {
|
|
1095
|
+
_startHistory();
|
|
1020
1096
|
} else {
|
|
1021
|
-
|
|
1097
|
+
pendingHistorySetupRef.current = _startHistory;
|
|
1022
1098
|
}
|
|
1023
|
-
|
|
1024
|
-
_phase2CleanupFn = function () { _phase2Active = false; _phase2Listener.dispose(); };
|
|
1025
1099
|
}
|
|
1026
1100
|
|
|
1027
1101
|
return function () {
|
|
@@ -1058,15 +1132,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1058
1132
|
}
|
|
1059
1133
|
if (window.__mbeditorActiveEditor === editor) {
|
|
1060
1134
|
window.__mbeditorActiveEditor = null;
|
|
1135
|
+
window.dispatchEvent(new CustomEvent('mbeditor:active-editor'));
|
|
1061
1136
|
}
|
|
1137
|
+
if (scrollSyncDisposable) scrollSyncDisposable.dispose();
|
|
1138
|
+
if (_modelEntry.editor === editor) _modelEntry.editor = null;
|
|
1062
1139
|
if (editorPluginDisposable) editorPluginDisposable.dispose();
|
|
1063
1140
|
if (formatActionDisposable) formatActionDisposable.dispose();
|
|
1064
1141
|
runTestAtCursorDisposable.dispose();
|
|
1065
1142
|
columnSelectDisposable.dispose();
|
|
1066
1143
|
contentDisposable.dispose();
|
|
1067
1144
|
EditorStore.setState({ canUndo: false, canRedo: false });
|
|
1145
|
+
pendingHistorySetupRef.current = null;
|
|
1068
1146
|
if (_phase2CleanupFn) _phase2CleanupFn();
|
|
1069
|
-
if (_collabActive) CollaborationService.unbindEditor(tab.path);
|
|
1147
|
+
if (_collabActive) CollaborationService.unbindEditor(tab.path, editor);
|
|
1070
1148
|
// Detach the model before disposing the editor so the model (and its undo
|
|
1071
1149
|
// history) survives for when the user returns to this tab.
|
|
1072
1150
|
editor.setModel(null);
|
|
@@ -1081,14 +1159,29 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1081
1159
|
var editor = monacoRef.current;
|
|
1082
1160
|
if (!editor || typeof tab.content !== 'string') return;
|
|
1083
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
|
+
|
|
1084
1173
|
var extVersion = tab.externalContentVersion || 0;
|
|
1085
|
-
if (extVersion <= lastAppliedExternalVersionRef.current)
|
|
1174
|
+
if (extVersion <= lastAppliedExternalVersionRef.current) {
|
|
1175
|
+
_runPendingHistory();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1086
1178
|
|
|
1087
1179
|
// For a collaboration late-join the shared document is authoritative; applying
|
|
1088
1180
|
// the disk content would clobber it. Mark this version consumed and skip.
|
|
1089
1181
|
if (collabActiveRef.current && typeof CollaborationService !== 'undefined' &&
|
|
1090
1182
|
CollaborationService.consumesDiskLoad(tab.path)) {
|
|
1091
1183
|
lastAppliedExternalVersionRef.current = extVersion;
|
|
1184
|
+
_runPendingHistory();
|
|
1092
1185
|
return;
|
|
1093
1186
|
}
|
|
1094
1187
|
|
|
@@ -1101,39 +1194,48 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1101
1194
|
// Normalize before comparing to prevent false positive dirty edits
|
|
1102
1195
|
var vNorm = editor.getValue().replace(/\r\n/g, '\n');
|
|
1103
1196
|
var cNorm = tab.content.replace(/\r\n/g, '\n');
|
|
1104
|
-
if (vNorm
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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);
|
|
1137
1239
|
}
|
|
1138
1240
|
}, [tab.content, tab.externalContentVersion]);
|
|
1139
1241
|
|
|
@@ -1219,7 +1321,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1219
1321
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1220
1322
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
1221
1323
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
1222
|
-
linkedEditing:
|
|
1324
|
+
linkedEditing: true,
|
|
1223
1325
|
acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on'
|
|
1224
1326
|
});
|
|
1225
1327
|
// Re-apply large-file tuning last so a prefs change can't re-enable the
|
|
@@ -1323,9 +1425,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1323
1425
|
var column = tab.gotoCol || 1;
|
|
1324
1426
|
var maxColumn = model.getLineMaxColumn(line);
|
|
1325
1427
|
if (column > maxColumn) column = maxColumn;
|
|
1428
|
+
// With both bounds the match itself is selected and the cursor ends up
|
|
1429
|
+
// just past it — where you'd start typing. With only gotoCol the
|
|
1430
|
+
// selection is empty, which is exactly setPosition.
|
|
1431
|
+
var endColumn = Math.min(tab.gotoEndCol || column, maxColumn);
|
|
1326
1432
|
|
|
1327
1433
|
editor.revealLineInCenter(line);
|
|
1328
|
-
|
|
1434
|
+
// setSelection(IRange) parks the cursor at the range's end, so no
|
|
1435
|
+
// setPosition afterwards — that would collapse the selection again.
|
|
1436
|
+
editor.setSelection({
|
|
1437
|
+
startLineNumber: line, startColumn: column,
|
|
1438
|
+
endLineNumber: line, endColumn: endColumn
|
|
1439
|
+
});
|
|
1329
1440
|
editor.focus();
|
|
1330
1441
|
|
|
1331
1442
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
@@ -1333,7 +1444,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1333
1444
|
}, 50);
|
|
1334
1445
|
|
|
1335
1446
|
return function () { clearTimeout(timer); };
|
|
1336
|
-
}, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
|
|
1447
|
+
}, [tab.gotoLine, tab.gotoCol, tab.gotoEndCol, tab.content, tab.loading]);
|
|
1337
1448
|
|
|
1338
1449
|
// Apply RuboCop markers
|
|
1339
1450
|
useEffect(function () {
|
|
@@ -1688,9 +1799,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1688
1799
|
// keystroke. This ticks 250 ms after the last change instead — the same
|
|
1689
1800
|
// treatment the route-hint redraw above already gets.
|
|
1690
1801
|
var _useStateZT = useState(0);
|
|
1691
|
-
var
|
|
1692
|
-
var
|
|
1693
|
-
var setZoneTick = _useStateZT2[1];
|
|
1802
|
+
var zoneTick = _useStateZT[0];
|
|
1803
|
+
var setZoneTick = _useStateZT[1];
|
|
1694
1804
|
useEffect(function () {
|
|
1695
1805
|
var timer = setTimeout(function () {
|
|
1696
1806
|
setZoneTick(function (n) { return n + 1; });
|
|
@@ -1803,74 +1913,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1803
1913
|
return src === derived;
|
|
1804
1914
|
};
|
|
1805
1915
|
|
|
1806
|
-
var testFileCandidates = function(relativePath) {
|
|
1807
|
-
if (!relativePath || !relativePath.endsWith('.rb')) return [];
|
|
1808
|
-
|
|
1809
|
-
var basename = relativePath.slice(0, -3);
|
|
1810
|
-
var dirParts = relativePath.split('/');
|
|
1811
|
-
var leafName = basename.split('/').pop();
|
|
1812
|
-
var candidates = [];
|
|
1813
|
-
|
|
1814
|
-
if (dirParts[0] === 'app' && dirParts.length > 1) {
|
|
1815
|
-
var subPath = dirParts.slice(1).join('/');
|
|
1816
|
-
var subDir = subPath.indexOf('/') !== -1 ? subPath.slice(0, subPath.lastIndexOf('/')) : '';
|
|
1817
|
-
candidates.push('test/' + (subDir ? subDir + '/' : '') + leafName + '_test.rb');
|
|
1818
|
-
candidates.push('spec/' + (subDir ? subDir + '/' : '') + leafName + '_spec.rb');
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
if (dirParts[0] === 'lib') {
|
|
1822
|
-
var libSubPath = dirParts.slice(1).join('/');
|
|
1823
|
-
var libSubDir = libSubPath.indexOf('/') !== -1 ? libSubPath.slice(0, libSubPath.lastIndexOf('/')) : '';
|
|
1824
|
-
candidates.push('test/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1825
|
-
candidates.push('test/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1826
|
-
candidates.push('spec/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_spec.rb');
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
candidates.push('test/' + leafName + '_test.rb');
|
|
1830
|
-
candidates.push('spec/' + leafName + '_spec.rb');
|
|
1831
|
-
|
|
1832
|
-
return candidates.filter(function(candidate, index, list) {
|
|
1833
|
-
return list.indexOf(candidate) === index;
|
|
1834
|
-
});
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
var treeHasPath = function(nodes, targetPath) {
|
|
1838
|
-
if (!targetPath) return false;
|
|
1839
|
-
var stack = (nodes || []).slice();
|
|
1840
|
-
|
|
1841
|
-
while (stack.length) {
|
|
1842
|
-
var node = stack.pop();
|
|
1843
|
-
if (!node) continue;
|
|
1844
|
-
if (node.path === targetPath) return true;
|
|
1845
|
-
if (node.children && node.children.length) {
|
|
1846
|
-
stack.push.apply(stack, node.children);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
return false;
|
|
1851
|
-
};
|
|
1852
|
-
|
|
1853
|
-
var matchingTestFilePath = function(sourcePath) {
|
|
1854
|
-
var normalized = (sourcePath || '').replace(/^\/+/, '');
|
|
1855
|
-
if (!normalized || !normalized.endsWith('.rb')) return null;
|
|
1856
|
-
if (/^(test|spec)\//.test(normalized) && /_(test|spec)\.rb$/.test(normalized)) return normalized;
|
|
1857
|
-
|
|
1858
|
-
var candidates = testFileCandidates(normalized);
|
|
1859
|
-
for (var i = 0; i < candidates.length; i += 1) {
|
|
1860
|
-
if (treeHasPath(treeData, candidates[i])) return candidates[i];
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
return null;
|
|
1864
|
-
};
|
|
1865
|
-
|
|
1866
|
-
// treeHasPath walks the whole file tree once per candidate, and the Test
|
|
1867
|
-
// button asked for this on every render — so once per keystroke. treeData is
|
|
1868
|
-
// deliberately outside the React.memo comparator below, so it can only change
|
|
1869
|
-
// on a render this component was going to do anyway.
|
|
1870
|
-
var testFileForTab = useMemo(function () {
|
|
1871
|
-
return matchingTestFilePath(tab.path);
|
|
1872
|
-
}, [tab.path, treeData]);
|
|
1873
|
-
|
|
1874
1916
|
// Map a test method name to the best-matching line in the source file.
|
|
1875
1917
|
// Extracts keywords from the test name and scores each source line.
|
|
1876
1918
|
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
@@ -2028,33 +2070,71 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2028
2070
|
if (isMarkdown && window.marked) {
|
|
2029
2071
|
(function () {
|
|
2030
2072
|
var renderer = new window.marked.Renderer();
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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;
|
|
2034
2077
|
renderer.html = function (token) {
|
|
2035
2078
|
return '<pre>' + escapeHtml(typeof token === 'object' ? token.text : token) + '</pre>';
|
|
2036
2079
|
};
|
|
2037
|
-
// Sanitize link/image hrefs to block javascript: and data: schemes.
|
|
2038
2080
|
var _origLink = renderer.link.bind(renderer);
|
|
2039
2081
|
var _origImage = renderer.image.bind(renderer);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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);
|
|
2044
2089
|
};
|
|
2045
|
-
renderer.
|
|
2046
|
-
|
|
2047
|
-
return _origLink(token);
|
|
2090
|
+
renderer.image = function(href, title, text) {
|
|
2091
|
+
return _origImage(safeHref(href), title, text);
|
|
2048
2092
|
};
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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';
|
|
2052
2107
|
};
|
|
2053
2108
|
setMarkup(window.marked.parse(markdownContent, { renderer: renderer }));
|
|
2054
2109
|
})();
|
|
2055
2110
|
}
|
|
2056
2111
|
}, [markdownContent, isMarkdown]);
|
|
2057
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
|
+
|
|
2058
2138
|
// Click-outside handler to close the methods dropdown
|
|
2059
2139
|
useEffect(function() {
|
|
2060
2140
|
if (!methodsOpen) return;
|
|
@@ -2164,7 +2244,12 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2164
2244
|
}
|
|
2165
2245
|
|
|
2166
2246
|
if (tab.isPreview && isMarkdown) {
|
|
2167
|
-
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
|
+
});
|
|
2168
2253
|
}
|
|
2169
2254
|
|
|
2170
2255
|
// Helper: shorten long paths by showing the last 2 segments with a leading ellipsis
|
|
@@ -2175,6 +2260,38 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2175
2260
|
return '\u2026/' + parts.slice(-2).join('/');
|
|
2176
2261
|
}
|
|
2177
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
|
+
|
|
2178
2295
|
// While Monaco is still loading, show a lightweight skeleton so the UI is
|
|
2179
2296
|
// visible immediately without calling monaco.editor.create() too early.
|
|
2180
2297
|
if (!monacoReady) {
|
|
@@ -2253,7 +2370,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2253
2370
|
React.createElement(
|
|
2254
2371
|
'span',
|
|
2255
2372
|
{ className: 'ide-editor-file-location', title: tab.path },
|
|
2256
|
-
|
|
2373
|
+
renderBreadcrumb(tab.path)
|
|
2257
2374
|
),
|
|
2258
2375
|
gitAvailable && tab.path && React.createElement(
|
|
2259
2376
|
'button',
|
|
@@ -2262,8 +2379,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2262
2379
|
onClick: function() { if (onShowHistory) onShowHistory(tab.path); },
|
|
2263
2380
|
title: 'File History'
|
|
2264
2381
|
},
|
|
2265
|
-
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.
|
|
2266
|
-
|
|
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')
|
|
2267
2384
|
),
|
|
2268
2385
|
hasOutline && React.createElement(
|
|
2269
2386
|
'button',
|
|
@@ -2317,8 +2434,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2317
2434
|
},
|
|
2318
2435
|
title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
|
|
2319
2436
|
},
|
|
2320
|
-
React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.
|
|
2321
|
-
|
|
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')
|
|
2322
2439
|
),
|
|
2323
2440
|
gitAvailable && React.createElement(
|
|
2324
2441
|
'button',
|
|
@@ -2327,21 +2444,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2327
2444
|
onClick: function() { setIsBlameVisible(function(prev) { return !prev; }); },
|
|
2328
2445
|
title: 'Toggle Git Blame'
|
|
2329
2446
|
},
|
|
2330
|
-
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.
|
|
2331
|
-
|
|
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')
|
|
2332
2449
|
),
|
|
2333
|
-
testAvailable && testFileForTab && React.createElement(
|
|
2334
|
-
'button',
|
|
2335
|
-
{
|
|
2336
|
-
className: 'ide-icon-btn',
|
|
2337
|
-
onClick: function() { if (onRunTest) onRunTest(); },
|
|
2338
|
-
disabled: testLoading,
|
|
2339
|
-
'aria-busy': !!testLoading,
|
|
2340
|
-
title: 'Run Tests'
|
|
2341
|
-
},
|
|
2342
|
-
!testLoading && React.createElement('i', { className: 'fas fa-flask', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2343
|
-
!editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
|
|
2344
|
-
)
|
|
2345
2450
|
),
|
|
2346
2451
|
conflictCount > 0 && React.createElement(
|
|
2347
2452
|
'div', { className: 'mb-conflict-banner' },
|
|
@@ -2460,7 +2565,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2460
2565
|
React.createElement(
|
|
2461
2566
|
'span',
|
|
2462
2567
|
{ style: { flex: 1, textAlign: 'center' } },
|
|
2463
|
-
'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' +
|
|
2568
|
+
'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + formatSize(pageTotalBytes) + ')'
|
|
2464
2569
|
),
|
|
2465
2570
|
React.createElement(
|
|
2466
2571
|
'button',
|
|
@@ -2502,7 +2607,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2502
2607
|
)
|
|
2503
2608
|
),
|
|
2504
2609
|
React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
|
|
2505
|
-
|
|
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(
|
|
2506
2614
|
'div',
|
|
2507
2615
|
{
|
|
2508
2616
|
ref: methodsDropdownRef,
|
|
@@ -2600,7 +2708,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2600
2708
|
}
|
|
2601
2709
|
return rows;
|
|
2602
2710
|
})()
|
|
2603
|
-
),
|
|
2711
|
+
), document.body),
|
|
2604
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' } })
|
|
2605
2713
|
);
|
|
2606
2714
|
};
|
|
@@ -2612,10 +2720,8 @@ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
|
|
|
2612
2720
|
prev.paneId === next.paneId &&
|
|
2613
2721
|
(prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
|
|
2614
2722
|
prev.gitAvailable === next.gitAvailable &&
|
|
2615
|
-
prev.testAvailable === next.testAvailable &&
|
|
2616
2723
|
prev.testResult === next.testResult &&
|
|
2617
2724
|
prev.testPanelFile === next.testPanelFile &&
|
|
2618
|
-
prev.testLoading === next.testLoading &&
|
|
2619
2725
|
prev.testInlineVisible === next.testInlineVisible &&
|
|
2620
2726
|
prev.editorPrefs === next.editorPrefs &&
|
|
2621
2727
|
prev.monacoReady === next.monacoReady;
|