mbeditor 0.12.9 → 0.13.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 +150 -0
- data/app/assets/javascripts/mbeditor/application.js +7 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
- data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +102 -25
- data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +598 -130
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +162 -6
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +427 -111
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +122 -18
- data/app/assets/javascripts/mbeditor/git_service.js +130 -8
- data/app/assets/javascripts/mbeditor/history_service.js +33 -38
- data/app/assets/javascripts/mbeditor/log_service.js +4 -0
- data/app/assets/javascripts/mbeditor/search_service.js +30 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +268 -53
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/editor_channel.rb +6 -3
- data/app/controllers/mbeditor/application_controller.rb +5 -0
- data/app/controllers/mbeditor/editors_controller.rb +173 -29
- data/app/controllers/mbeditor/git_controller.rb +3 -3
- data/app/controllers/mbeditor/logs_controller.rb +3 -1
- data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
- data/app/services/mbeditor/editor_state_service.rb +36 -26
- data/app/services/mbeditor/exclusion_matcher.rb +12 -10
- data/app/services/mbeditor/file_operation_service.rb +38 -3
- data/app/services/mbeditor/file_tree_service.rb +30 -2
- data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
- data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
- data/app/services/mbeditor/git_diff_service.rb +5 -1
- data/app/services/mbeditor/git_info_service.rb +20 -8
- data/app/services/mbeditor/git_line_diff_service.rb +6 -2
- data/app/services/mbeditor/git_service.rb +65 -15
- data/app/services/mbeditor/js_definition_service.rb +3 -1
- data/app/services/mbeditor/js_globals_service.rb +7 -4
- data/app/services/mbeditor/js_members_service.rb +3 -2
- data/app/services/mbeditor/js_program_service.rb +15 -5
- data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
- data/app/services/mbeditor/process_runner.rb +42 -12
- data/app/services/mbeditor/ri_definition_service.rb +8 -1
- data/app/services/mbeditor/route_service.rb +45 -8
- data/app/services/mbeditor/rubocop_run_service.rb +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +65 -64
- data/app/services/mbeditor/search_replace_service.rb +31 -5
- data/app/services/mbeditor/test_runner_service.rb +98 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +12 -1
- data/lib/mbeditor/editor_bootstrap.rb +18 -12
- data/lib/mbeditor/engine.rb +22 -3
- data/lib/mbeditor/exception_log.rb +2 -2
- data/lib/mbeditor/pending_migrations.rb +33 -0
- data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
- data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
- data/lib/mbeditor/route_map.rb +2 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- metadata +7 -2
|
@@ -10,6 +10,7 @@ var _React = React;
|
|
|
10
10
|
var useState = _React.useState;
|
|
11
11
|
var useEffect = _React.useEffect;
|
|
12
12
|
var useRef = _React.useRef;
|
|
13
|
+
var useMemo = _React.useMemo;
|
|
13
14
|
|
|
14
15
|
// Functional setTreeData updater shared by every path that re-fetches the tree
|
|
15
16
|
// (WebSocket push, the 10s poll, the manual refresh button).
|
|
@@ -109,7 +110,6 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
109
110
|
autoClosingBrackets: 'always',
|
|
110
111
|
autoClosingQuotes: 'always',
|
|
111
112
|
autoIndent: 'full',
|
|
112
|
-
indentOnPaste: true,
|
|
113
113
|
formatOnType: false,
|
|
114
114
|
formatOnSave: false,
|
|
115
115
|
quickSuggestions: true,
|
|
@@ -118,6 +118,7 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
118
118
|
autoRevealInExplorer: true,
|
|
119
119
|
toolbarIconOnly: false,
|
|
120
120
|
rubocopLintEnabled: true,
|
|
121
|
+
routeHints: true,
|
|
121
122
|
prettierPrintWidth: 80,
|
|
122
123
|
prettierSemi: true,
|
|
123
124
|
prettierSingleQuote: false,
|
|
@@ -169,18 +170,32 @@ var SidebarActionButton = function SidebarActionButton(_ref) {
|
|
|
169
170
|
var _ref$ariaBusy = _ref.ariaBusy;
|
|
170
171
|
var ariaBusy = _ref$ariaBusy === undefined ? false : _ref$ariaBusy;
|
|
171
172
|
|
|
173
|
+
// The tooltip lives on a wrapper, not on the button, and is drawn by CSS
|
|
174
|
+
// rather than the title attribute. Both parts are load-bearing:
|
|
175
|
+
//
|
|
176
|
+
// The host app's Pico CSS sets `pointer-events: none` on [disabled], so a
|
|
177
|
+
// disabled button never receives hover and a native title never appears at
|
|
178
|
+
// all — and Rename/Delete are disabled whenever nothing is selected, which
|
|
179
|
+
// is most of the time. Hanging the tooltip on an always-enabled wrapper is
|
|
180
|
+
// what keeps it working in that state.
|
|
181
|
+
//
|
|
182
|
+
// Native titles are also ~1s late, which is already why .collab-hovercard
|
|
183
|
+
// exists rather than a title on the collab chip.
|
|
172
184
|
return React.createElement(
|
|
173
|
-
"
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
"span",
|
|
186
|
+
{ className: "sb-tip", "data-tip": title },
|
|
187
|
+
React.createElement(
|
|
188
|
+
"button",
|
|
189
|
+
{
|
|
190
|
+
type: "button",
|
|
191
|
+
className: "project-action-btn" + (danger ? " danger" : ""),
|
|
192
|
+
"aria-label": ariaLabel || title,
|
|
193
|
+
"aria-busy": !!ariaBusy,
|
|
194
|
+
onClick: onClick,
|
|
195
|
+
disabled: !!disabled
|
|
196
|
+
},
|
|
197
|
+
!ariaBusy && React.createElement("i", { className: iconClass })
|
|
198
|
+
)
|
|
184
199
|
);
|
|
185
200
|
};
|
|
186
201
|
|
|
@@ -347,6 +362,49 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
347
362
|
var searchWholeWordRef = useRef(false);
|
|
348
363
|
var searchResultsContainerRef = useRef(null);
|
|
349
364
|
|
|
365
|
+
// Search results are windowed: a project-wide query can return up to
|
|
366
|
+
// SearchReplaceService::MAX_RESULTS (10_000) rows, and every row is five
|
|
367
|
+
// elements, so rendering the list in full built ~50k nodes — enough to kill
|
|
368
|
+
// the tab outright, and 92 ms of render for a mere 3_000 rows. Rows are a
|
|
369
|
+
// fixed 40px (.search-result-item), which is what makes the arithmetic here
|
|
370
|
+
// as simple as the file tree's.
|
|
371
|
+
var SEARCH_ROW_HEIGHT = 40;
|
|
372
|
+
var SEARCH_ROW_BUFFER = 5;
|
|
373
|
+
var _useStateSV = useState({ scrollTop: 0, height: 0 });
|
|
374
|
+
var _useStateSV2 = _slicedToArray(_useStateSV, 2);
|
|
375
|
+
var searchViewport = _useStateSV2[0];
|
|
376
|
+
var setSearchViewport = _useStateSV2[1];
|
|
377
|
+
|
|
378
|
+
// The container's height is only known once it is on screen, and a viewport
|
|
379
|
+
// of 0 would render a single row and never grow — nothing would scroll, so
|
|
380
|
+
// no scroll event would arrive to correct it.
|
|
381
|
+
//
|
|
382
|
+
// Measured from a callback ref rather than an effect: the results list
|
|
383
|
+
// mounts and unmounts as the sidebar tab changes and as a query goes from
|
|
384
|
+
// no-results to results, none of which an effect's dependency list sees.
|
|
385
|
+
// Same reason the model graph wires its pan/zoom this way.
|
|
386
|
+
var searchViewportObserverRef = useRef(null);
|
|
387
|
+
var measureSearchViewport = function measureSearchViewport(el) {
|
|
388
|
+
if (!el) return;
|
|
389
|
+
setSearchViewport(function (prev) {
|
|
390
|
+
if (prev.height === el.clientHeight && prev.scrollTop === el.scrollTop) return prev;
|
|
391
|
+
return { scrollTop: el.scrollTop, height: el.clientHeight };
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
var attachSearchResults = useRef(function (el) {
|
|
395
|
+
if (searchViewportObserverRef.current) {
|
|
396
|
+
searchViewportObserverRef.current.disconnect();
|
|
397
|
+
searchViewportObserverRef.current = null;
|
|
398
|
+
}
|
|
399
|
+
searchResultsContainerRef.current = el;
|
|
400
|
+
if (!el) return;
|
|
401
|
+
measureSearchViewport(el);
|
|
402
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
403
|
+
var obs = new ResizeObserver(function () { measureSearchViewport(el); });
|
|
404
|
+
obs.observe(el);
|
|
405
|
+
searchViewportObserverRef.current = obs;
|
|
406
|
+
}).current;
|
|
407
|
+
|
|
350
408
|
var _useStateRM = useState(false);
|
|
351
409
|
var _useStateRM2 = _slicedToArray(_useStateRM, 2);
|
|
352
410
|
var replaceMode = _useStateRM2[0];
|
|
@@ -408,6 +466,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
408
466
|
var importConflict = _useStateImportConflict2[0];
|
|
409
467
|
var setImportConflict = _useStateImportConflict2[1];
|
|
410
468
|
|
|
469
|
+
// { initialFolder } while the upload dialog is open, null otherwise.
|
|
470
|
+
var _useStateImportDialog = useState(null);
|
|
471
|
+
var _useStateImportDialog2 = _slicedToArray(_useStateImportDialog, 2);
|
|
472
|
+
var importDialog = _useStateImportDialog2[0];
|
|
473
|
+
var setImportDialog = _useStateImportDialog2[1];
|
|
474
|
+
|
|
411
475
|
var _useStateSchemaLoading = useState(null);
|
|
412
476
|
var _useStateSchemaLoading2 = _slicedToArray(_useStateSchemaLoading, 2);
|
|
413
477
|
var schemaLoadingLabel = _useStateSchemaLoading2[0];
|
|
@@ -497,6 +561,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
497
561
|
var showProblemsPanel = _useStateProblems2[0];
|
|
498
562
|
var setShowProblemsPanel = _useStateProblems2[1];
|
|
499
563
|
|
|
564
|
+
var _useStateTestRun = useState(false);
|
|
565
|
+
var _useStateTestRun2 = _slicedToArray(_useStateTestRun, 2);
|
|
566
|
+
var showTestRunPanel = _useStateTestRun2[0];
|
|
567
|
+
var setShowTestRunPanel = _useStateTestRun2[1];
|
|
568
|
+
|
|
500
569
|
// Error/warning tallies across the open tabs, mirrored into the status bar.
|
|
501
570
|
var _useStateProblemCounts = useState({ errors: 0, warnings: 0 });
|
|
502
571
|
var _useStateProblemCounts2 = _slicedToArray(_useStateProblemCounts, 2);
|
|
@@ -757,6 +826,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
757
826
|
if (typeof content === 'string') {
|
|
758
827
|
lastDiskContentRef.current[path] = content.replace(/\r\n/g, '\n');
|
|
759
828
|
}
|
|
829
|
+
// The file on disk just moved, so any line-diff answer we are holding for
|
|
830
|
+
// it predates the write and must not be reused for the tint.
|
|
831
|
+
if (typeof GitService !== 'undefined' && GitService.invalidateLineDiff) {
|
|
832
|
+
GitService.invalidateLineDiff(path);
|
|
833
|
+
}
|
|
760
834
|
}
|
|
761
835
|
|
|
762
836
|
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
@@ -821,6 +895,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
821
895
|
});
|
|
822
896
|
};
|
|
823
897
|
|
|
898
|
+
// filterDotFiles rebuilds the whole tree, so calling it inline in the render
|
|
899
|
+
// handed FileTreeMemo a fresh array every time and its `prev.items ===
|
|
900
|
+
// next.items` check never held — the memo was there but never fired.
|
|
901
|
+
var fileTreeItems = useMemo(function () {
|
|
902
|
+
return editorPrefs.showDotFiles ? treeData : filterDotFiles(treeData || []);
|
|
903
|
+
}, [treeData, editorPrefs.showDotFiles]);
|
|
904
|
+
|
|
824
905
|
var normalizeRelativePath = function normalizeRelativePath(input) {
|
|
825
906
|
return (input || "").replace(/\\/g, "/").trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/+/g, "/");
|
|
826
907
|
};
|
|
@@ -863,10 +944,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
863
944
|
// cached page was served for the same query. The per-path delta refresh on
|
|
864
945
|
// the files_changed push can't cover it either: it re-scans named files,
|
|
865
946
|
// and a file that just appeared or vanished isn't in the previous result set.
|
|
947
|
+
// True when the server will push a files_changed broadcast for a write we are
|
|
948
|
+
// about to make, so the coalesced handler will refresh the tree and git for
|
|
949
|
+
// us. Every mutation used to refresh both itself as well, which doubled or
|
|
950
|
+
// tripled the most expensive requests the editor makes. Without a socket
|
|
951
|
+
// nothing arrives, so callers keep their own refresh for that case.
|
|
952
|
+
var _socketWillBroadcast = function _socketWillBroadcast() {
|
|
953
|
+
return typeof WebSocketService !== 'undefined' &&
|
|
954
|
+
typeof WebSocketService.isCableAvailable === 'function' &&
|
|
955
|
+
WebSocketService.isCableAvailable();
|
|
956
|
+
};
|
|
957
|
+
|
|
866
958
|
var refreshProjectTree = function refreshProjectTree() {
|
|
867
959
|
return FileService.getTree().then(function (data) {
|
|
868
|
-
|
|
869
|
-
|
|
960
|
+
// _treeUpdater keeps the previous array (and skips the index rebuild)
|
|
961
|
+
// when the tree is unchanged; going round it re-rendered the whole app.
|
|
962
|
+
setTreeData(_treeUpdater(data || []));
|
|
870
963
|
SearchService.invalidate();
|
|
871
964
|
if (searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
872
965
|
_debouncedSearch(searchQueryRef.current);
|
|
@@ -988,15 +1081,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
988
1081
|
});
|
|
989
1082
|
};
|
|
990
1083
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1084
|
+
// The one writer for the markers map. Two things it must not do:
|
|
1085
|
+
//
|
|
1086
|
+
// * write a fresh map when nothing changed — the auto-lint fires per
|
|
1087
|
+
// keystroke on JS files and re-clearing an already-empty marker list
|
|
1088
|
+
// would re-render the whole app every time;
|
|
1089
|
+
// * mutate the tab object in the store — EditorStore's contract is that
|
|
1090
|
+
// every nested value is replaced, never edited in place, or
|
|
1091
|
+
// subscribeToSlice cannot see the change. TabBar reads this map instead.
|
|
1092
|
+
var applyMarkersForTab = function applyMarkersForTab(tabId, nextMarkers) {
|
|
999
1093
|
setMarkers(function (prev) {
|
|
1094
|
+
var cur = prev[tabId];
|
|
1095
|
+
if (cur && cur.length === 0 && nextMarkers.length === 0) return prev;
|
|
1000
1096
|
return _extends({}, prev, _defineProperty({}, tabId, nextMarkers));
|
|
1001
1097
|
});
|
|
1002
1098
|
};
|
|
@@ -1006,6 +1102,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1006
1102
|
|
|
1007
1103
|
if (!tab || (!isRubyPath(tab.path) && !tab.path.endsWith('.haml'))) return Promise.resolve(null);
|
|
1008
1104
|
|
|
1105
|
+
// An empty buffer has no offenses, and answering that here rather than over
|
|
1106
|
+
// the wire skips the most expensive request the Ruby path makes: a
|
|
1107
|
+
// whole-document RuboCop run, budgeted at 10s server-side. It matters
|
|
1108
|
+
// because every newly created .rb file starts empty and is opened
|
|
1109
|
+
// immediately, so the lint fired on a document with nothing in it.
|
|
1110
|
+
// Resolving with an empty marker set (rather than null) keeps the
|
|
1111
|
+
// marker-clearing below intact, so deleting a file's contents still clears
|
|
1112
|
+
// its squiggles.
|
|
1113
|
+
if (!String(tab.content || '').trim()) {
|
|
1114
|
+
applyMarkersForTab(tab.id, []);
|
|
1115
|
+
if (options.showStatus) EditorStore.setStatus('No RuboCop offenses!', 'success');
|
|
1116
|
+
return Promise.resolve({ markers: [] });
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1009
1119
|
if (options.showLoading) {
|
|
1010
1120
|
setLoading(function (prev) {
|
|
1011
1121
|
return _extends({}, prev, { lint: true });
|
|
@@ -1037,7 +1147,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1037
1147
|
|
|
1038
1148
|
return lintRequest.then(function (res) {
|
|
1039
1149
|
var nextMarkers = res.markers || [];
|
|
1040
|
-
applyMarkersForTab(
|
|
1150
|
+
applyMarkersForTab(tab.id, nextMarkers);
|
|
1041
1151
|
|
|
1042
1152
|
if (options.showStatus) {
|
|
1043
1153
|
var count = res.summary && res.summary.offense_count || 0;
|
|
@@ -1075,16 +1185,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1075
1185
|
if (parserName && window.prettier && window.prettierPlugins) {
|
|
1076
1186
|
var prefs = EditorStore.getState().editorPrefs || DEFAULT_EDITOR_PREFS;
|
|
1077
1187
|
window.prettier.format(tab.content, prettierOptions(prefs, parserName)).then(function () {
|
|
1078
|
-
|
|
1079
|
-
return p.id === paneId;
|
|
1080
|
-
});
|
|
1081
|
-
var current = currentPane ? currentPane.tabs.find(function (t) {
|
|
1082
|
-
return t.id === tab.id;
|
|
1083
|
-
}) : null;
|
|
1084
|
-
if (current) current.markers = [];
|
|
1085
|
-
setMarkers(function (prev) {
|
|
1086
|
-
return _extends({}, prev, _defineProperty({}, tab.id, []));
|
|
1087
|
-
});
|
|
1188
|
+
applyMarkersForTab(tab.id, []);
|
|
1088
1189
|
})["catch"](function (err) {
|
|
1089
1190
|
var newMarkers = [];
|
|
1090
1191
|
if (err && err.loc) {
|
|
@@ -1101,16 +1202,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1101
1202
|
endCol: endLoc ? endLoc.column : loc.column + 1
|
|
1102
1203
|
});
|
|
1103
1204
|
}
|
|
1104
|
-
|
|
1105
|
-
return p.id === paneId;
|
|
1106
|
-
});
|
|
1107
|
-
var current = currentPane ? currentPane.tabs.find(function (t) {
|
|
1108
|
-
return t.id === tab.id;
|
|
1109
|
-
}) : null;
|
|
1110
|
-
if (current) current.markers = newMarkers;
|
|
1111
|
-
setMarkers(function (prev) {
|
|
1112
|
-
return _extends({}, prev, _defineProperty({}, tab.id, newMarkers));
|
|
1113
|
-
});
|
|
1205
|
+
applyMarkersForTab(tab.id, newMarkers);
|
|
1114
1206
|
});
|
|
1115
1207
|
}
|
|
1116
1208
|
}, 600)).current;
|
|
@@ -1168,6 +1260,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1168
1260
|
if (workspace && typeof workspace.redmineEnabled === 'boolean') {
|
|
1169
1261
|
setRedmineEnabled(workspace.redmineEnabled);
|
|
1170
1262
|
}
|
|
1263
|
+
if (workspace && (workspace.testTimeout || workspace.testAllTimeout)) {
|
|
1264
|
+
FileService.setTestTimeouts({
|
|
1265
|
+
test: workspace.testTimeout,
|
|
1266
|
+
testAll: workspace.testAllTimeout
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1171
1269
|
if (workspace && typeof workspace.testAvailable === 'boolean') {
|
|
1172
1270
|
setTestAvailable(workspace.testAvailable);
|
|
1173
1271
|
}
|
|
@@ -1683,8 +1781,32 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1683
1781
|
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1684
1782
|
// and git status immediately (same work as the 10s poll below does).
|
|
1685
1783
|
useEffect(function () {
|
|
1686
|
-
|
|
1687
|
-
|
|
1784
|
+
// One broadcast per written file, so a bulk write (Save All, Format All,
|
|
1785
|
+
// a drag-and-drop import, a rename) used to cost one full tree walk and
|
|
1786
|
+
// one git status PER FILE, all in flight at once. On a real host repo
|
|
1787
|
+
// those are the two most expensive requests the editor makes, and with
|
|
1788
|
+
// the browser's six sockets per host the saves themselves ended up queued
|
|
1789
|
+
// behind their own fallout — past ~30 files the POSTs hit the 30 s axios
|
|
1790
|
+
// timeout and Save All reported failure for writes that had succeeded.
|
|
1791
|
+
// The refresh is idempotent, so a burst only needs one of each.
|
|
1792
|
+
var timer = null;
|
|
1793
|
+
var pendingPaths = [];
|
|
1794
|
+
var pendingFullCheck = false;
|
|
1795
|
+
var pendingStructural = false;
|
|
1796
|
+
|
|
1797
|
+
function flush() {
|
|
1798
|
+
timer = null;
|
|
1799
|
+
// The named paths and "re-check everything" are two separate questions.
|
|
1800
|
+
// Collapsing them lost the paths whenever a path-less broadcast (a new
|
|
1801
|
+
// directory, say) landed in the same window as a save, and the saved
|
|
1802
|
+
// file's search rows then stayed stale until the next edit.
|
|
1803
|
+
var paths = pendingPaths;
|
|
1804
|
+
var fullCheck = pendingFullCheck;
|
|
1805
|
+
var structural = pendingStructural;
|
|
1806
|
+
pendingPaths = [];
|
|
1807
|
+
pendingFullCheck = false;
|
|
1808
|
+
pendingStructural = false;
|
|
1809
|
+
var tabsToCheck = (fullCheck || paths.length === 0) ? null : paths;
|
|
1688
1810
|
// The cheap /git_status probe, not the full /git_info fan-out. This
|
|
1689
1811
|
// fires on every save, and the fan-out is the most expensive request
|
|
1690
1812
|
// the editor makes — on a dev server with a handful of threads it
|
|
@@ -1693,17 +1815,75 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1693
1815
|
// branch and file list immediately and escalates to the fan-out on its
|
|
1694
1816
|
// own when the branch actually changed.
|
|
1695
1817
|
GitService.fetchStatusLite({ background: true })["catch"](function () {});
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1818
|
+
|
|
1819
|
+
// A save changes a file's contents, not the shape of the workspace, so
|
|
1820
|
+
// there is nothing in the tree for it to invalidate: the names, the
|
|
1821
|
+
// nesting and the quick-open index are all exactly as they were. Walking
|
|
1822
|
+
// the whole workspace to learn that — and then rebuilding the MiniSearch
|
|
1823
|
+
// index from the result, because the file's byte count moved and made
|
|
1824
|
+
// the payload compare unequal — was pure cost on the save path, and it
|
|
1825
|
+
// grew with the size of the checkout rather than with the edit.
|
|
1826
|
+
// Git badges come from git_status above, not from this payload.
|
|
1827
|
+
if (structural) {
|
|
1828
|
+
FileService.getTree().then(function (data) {
|
|
1829
|
+
setTreeData(_treeUpdater(data || []));
|
|
1830
|
+
checkOpenTabsForExternalChanges(tabsToCheck);
|
|
1831
|
+
})["catch"](function () {});
|
|
1832
|
+
} else {
|
|
1833
|
+
checkOpenTabsForExternalChanges(tabsToCheck);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (paths.length && searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
1837
|
+
paths.forEach(function (p) { _pendingSearchRefreshPaths.current.add(p); });
|
|
1702
1838
|
_flushSearchRefresh();
|
|
1703
1839
|
}
|
|
1704
1840
|
}
|
|
1841
|
+
|
|
1842
|
+
function handleFilesChanged(payload) {
|
|
1843
|
+
if (document.hidden) return;
|
|
1844
|
+
// No paths named means "something changed, re-check everything" — it
|
|
1845
|
+
// must not be swallowed by a burst that did name paths.
|
|
1846
|
+
if (!payload || !payload.paths) pendingFullCheck = true;
|
|
1847
|
+
else pendingPaths = pendingPaths.concat(payload.paths);
|
|
1848
|
+
// Absent (an older server) counts as structural — the conservative read.
|
|
1849
|
+
if (!payload || payload.structural !== false) pendingStructural = true;
|
|
1850
|
+
if (timer) return;
|
|
1851
|
+
timer = setTimeout(flush, 200);
|
|
1852
|
+
}
|
|
1853
|
+
// Deferring the refresh means it can now come due after the page has begun
|
|
1854
|
+
// going away. Requests issued into a closing page are aborted mid-flight,
|
|
1855
|
+
// which wastes a tree walk and leaves the server holding a connection that
|
|
1856
|
+
// never completes.
|
|
1857
|
+
var cancelPending = function () {
|
|
1858
|
+
if (timer) clearTimeout(timer);
|
|
1859
|
+
timer = null;
|
|
1860
|
+
};
|
|
1861
|
+
window.addEventListener('pagehide', cancelPending);
|
|
1862
|
+
|
|
1705
1863
|
WebSocketService.onFilesChanged(handleFilesChanged);
|
|
1706
|
-
return function () {
|
|
1864
|
+
return function () {
|
|
1865
|
+
cancelPending();
|
|
1866
|
+
window.removeEventListener('pagehide', cancelPending);
|
|
1867
|
+
WebSocketService.offFilesChanged(handleFilesChanged);
|
|
1868
|
+
};
|
|
1869
|
+
}, []);
|
|
1870
|
+
|
|
1871
|
+
// A branch switch changes every tracked file at once, and no push announces
|
|
1872
|
+
// it — the broadcast only covers mbeditor's own writes. Re-read the tree and
|
|
1873
|
+
// every open tab, the same work a manual workspace refresh does. Clean tabs
|
|
1874
|
+
// take the new branch's content; dirty ones queue the usual reload prompt,
|
|
1875
|
+
// so unsaved work is never overwritten.
|
|
1876
|
+
useEffect(function () {
|
|
1877
|
+
function onBranchChanged() {
|
|
1878
|
+
SearchService.invalidate();
|
|
1879
|
+
FileService.getTree({ refresh: true }).then(function (data) {
|
|
1880
|
+
setTreeData(_treeUpdater(data || []));
|
|
1881
|
+
})["catch"](function () {})["finally"](function () {
|
|
1882
|
+
checkOpenTabsForExternalChanges();
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
window.addEventListener('mbeditor:branch-changed', onBranchChanged);
|
|
1886
|
+
return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
|
|
1707
1887
|
}, []);
|
|
1708
1888
|
|
|
1709
1889
|
// WebSocket push — when a peer saves a collaboratively-bound file, the CRDT has
|
|
@@ -1926,7 +2106,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1926
2106
|
if (timer) clearTimeout(timer);
|
|
1927
2107
|
timer = setTimeout(function () {
|
|
1928
2108
|
timer = null;
|
|
1929
|
-
|
|
2109
|
+
var next = window.ProblemsPanel.counts();
|
|
2110
|
+
// Two object literals are never Object.is-equal, so handing React a
|
|
2111
|
+
// fresh {errors, warnings} re-rendered the app on every marker change
|
|
2112
|
+
// — which for a JS file is every keystroke. Compare the fields.
|
|
2113
|
+
setProblemCounts(function (prev) {
|
|
2114
|
+
return (prev && prev.errors === next.errors && prev.warnings === next.warnings) ? prev : next;
|
|
2115
|
+
});
|
|
1930
2116
|
}, 250);
|
|
1931
2117
|
};
|
|
1932
2118
|
|
|
@@ -1968,6 +2154,33 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1968
2154
|
handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
|
|
1969
2155
|
};
|
|
1970
2156
|
|
|
2157
|
+
// Both of these are keyed by something that dies with the tab — markers by
|
|
2158
|
+
// tab id, the external-change baseline by path — and nothing dropped them, so
|
|
2159
|
+
// a long session held a full copy of every file it had ever opened. Run after
|
|
2160
|
+
// a close (single or bulk) and reconcile against what is still open: a path
|
|
2161
|
+
// the other pane still shows keeps its baseline, which a per-tab delete
|
|
2162
|
+
// would have got wrong.
|
|
2163
|
+
var forgetClosedTabs = function forgetClosedTabs() {
|
|
2164
|
+
var openIds = {};
|
|
2165
|
+
var openPaths = {};
|
|
2166
|
+
EditorStore.getState().panes.forEach(function (p) {
|
|
2167
|
+
p.tabs.forEach(function (t) {
|
|
2168
|
+
openIds[t.id] = true;
|
|
2169
|
+
if (t.path) openPaths[t.path] = true;
|
|
2170
|
+
});
|
|
2171
|
+
});
|
|
2172
|
+
Object.keys(lastDiskContentRef.current).forEach(function (path) {
|
|
2173
|
+
if (!openPaths[path]) delete lastDiskContentRef.current[path];
|
|
2174
|
+
});
|
|
2175
|
+
setMarkers(function (prev) {
|
|
2176
|
+
var stale = Object.keys(prev).filter(function (id) { return !openIds[id]; });
|
|
2177
|
+
if (stale.length === 0) return prev;
|
|
2178
|
+
var next = _extends({}, prev);
|
|
2179
|
+
stale.forEach(function (id) { delete next[id]; });
|
|
2180
|
+
return next;
|
|
2181
|
+
});
|
|
2182
|
+
};
|
|
2183
|
+
|
|
1971
2184
|
var requestCloseTab = function requestCloseTab(paneId, id) {
|
|
1972
2185
|
var pane = state.panes.find(function (p) {
|
|
1973
2186
|
return p.id === paneId;
|
|
@@ -1987,6 +2200,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1987
2200
|
});
|
|
1988
2201
|
})
|
|
1989
2202
|
});
|
|
2203
|
+
forgetClosedTabs();
|
|
1990
2204
|
}
|
|
1991
2205
|
};
|
|
1992
2206
|
|
|
@@ -2040,6 +2254,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2040
2254
|
});
|
|
2041
2255
|
})
|
|
2042
2256
|
});
|
|
2257
|
+
forgetClosedTabs();
|
|
2043
2258
|
})["catch"](function (err) {
|
|
2044
2259
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2045
2260
|
})["finally"](function () {
|
|
@@ -2059,6 +2274,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2059
2274
|
});
|
|
2060
2275
|
})
|
|
2061
2276
|
});
|
|
2277
|
+
forgetClosedTabs();
|
|
2062
2278
|
setClosingTabId(null);
|
|
2063
2279
|
setClosingPaneId(null);
|
|
2064
2280
|
}
|
|
@@ -2083,6 +2299,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2083
2299
|
if (!confirmBulkClose(allTabs, "all editors")) return;
|
|
2084
2300
|
|
|
2085
2301
|
TabManager.closeAllTabs();
|
|
2302
|
+
forgetClosedTabs();
|
|
2086
2303
|
setClosingTabId(null);
|
|
2087
2304
|
setClosingPaneId(null);
|
|
2088
2305
|
EditorStore.setStatus("Closed " + allTabs.length + " editor" + (allTabs.length === 1 ? "" : "s"), "info");
|
|
@@ -2096,6 +2313,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2096
2313
|
if (!confirmBulkClose(pane.tabs, "all editors in Group " + paneId)) return;
|
|
2097
2314
|
|
|
2098
2315
|
TabManager.closeAllTabsInPane(paneId);
|
|
2316
|
+
forgetClosedTabs();
|
|
2099
2317
|
setClosingTabId(null);
|
|
2100
2318
|
setClosingPaneId(null);
|
|
2101
2319
|
EditorStore.setStatus("Closed " + pane.tabs.length + " editor" + (pane.tabs.length === 1 ? "" : "s") + " in Group " + paneId, "info");
|
|
@@ -2108,6 +2326,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2108
2326
|
if (others.length === 0) return;
|
|
2109
2327
|
if (!confirmBulkClose(others, "other editors")) return;
|
|
2110
2328
|
TabManager.closeOtherTabsInPane(paneId, keepId);
|
|
2329
|
+
forgetClosedTabs();
|
|
2111
2330
|
EditorStore.setStatus("Closed " + others.length + " editor" + (others.length === 1 ? "" : "s"), "info");
|
|
2112
2331
|
};
|
|
2113
2332
|
|
|
@@ -2117,6 +2336,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2117
2336
|
var saved = pane.tabs.filter(function (t) { return !t.dirty; });
|
|
2118
2337
|
if (saved.length === 0) return;
|
|
2119
2338
|
TabManager.closeSavedTabsInPane(paneId);
|
|
2339
|
+
forgetClosedTabs();
|
|
2120
2340
|
EditorStore.setStatus("Closed " + saved.length + " saved editor" + (saved.length === 1 ? "" : "s"), "info");
|
|
2121
2341
|
};
|
|
2122
2342
|
|
|
@@ -2134,6 +2354,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2134
2354
|
GitService.fetchStatusLite({ background: true });
|
|
2135
2355
|
FileService.getTree().then(function (data) { setTreeData(_treeUpdater(data || [])); })["catch"](function () {});
|
|
2136
2356
|
TabManager.closeTab(paneId, tab.id);
|
|
2357
|
+
forgetClosedTabs();
|
|
2137
2358
|
if (!(opts && opts.close)) {
|
|
2138
2359
|
TabManager.openTab(newPath, newPath.split('/').pop(), null, paneId);
|
|
2139
2360
|
}
|
|
@@ -2296,7 +2517,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2296
2517
|
var RAILS_MAX_RESOURCES = 10;
|
|
2297
2518
|
|
|
2298
2519
|
// Map resource label → representative path (capped at RAILS_MAX_RESOURCES, focused pane first)
|
|
2299
|
-
|
|
2520
|
+
// Memoised: all three of these walk every tab in every pane and were rebuilt
|
|
2521
|
+
// on every render, including the ones a keystroke causes.
|
|
2522
|
+
// resourceLabelFromPath reads customPathsRef, hence customPaths in the deps.
|
|
2523
|
+
var railsResourceDeps = useMemo(function() {
|
|
2300
2524
|
var deps = {};
|
|
2301
2525
|
var panesOrdered = state.panes.slice().sort(function(a, b) {
|
|
2302
2526
|
return a.id === state.focusedPaneId ? -1 : b.id === state.focusedPaneId ? 1 : 0;
|
|
@@ -2313,10 +2537,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2313
2537
|
});
|
|
2314
2538
|
});
|
|
2315
2539
|
return deps;
|
|
2316
|
-
})
|
|
2540
|
+
}, [state.panes, state.focusedPaneId, customPaths]);
|
|
2317
2541
|
var railsResourceDepStr = Object.keys(railsResourceDeps).sort().join('|');
|
|
2318
2542
|
|
|
2319
|
-
var railsOverflow = (function() {
|
|
2543
|
+
var railsOverflow = useMemo(function() {
|
|
2320
2544
|
var all = {};
|
|
2321
2545
|
state.panes.forEach(function(p) {
|
|
2322
2546
|
p.tabs.forEach(function(t) {
|
|
@@ -2326,9 +2550,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2326
2550
|
});
|
|
2327
2551
|
});
|
|
2328
2552
|
return Math.max(0, Object.keys(all).length - Object.keys(railsResourceDeps).length);
|
|
2329
|
-
})
|
|
2553
|
+
}, [state.panes, railsResourceDeps, customPaths]);
|
|
2330
2554
|
|
|
2331
|
-
var dirtyPaths = (function() {
|
|
2555
|
+
var dirtyPaths = useMemo(function() {
|
|
2332
2556
|
var set = {};
|
|
2333
2557
|
state.panes.forEach(function(p) {
|
|
2334
2558
|
p.tabs.forEach(function(t) {
|
|
@@ -2336,7 +2560,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2336
2560
|
});
|
|
2337
2561
|
});
|
|
2338
2562
|
return set;
|
|
2339
|
-
})
|
|
2563
|
+
}, [state.panes]);
|
|
2340
2564
|
|
|
2341
2565
|
useEffect(function() {
|
|
2342
2566
|
if (activeSidebarTab !== 'rails') return;
|
|
@@ -2552,17 +2776,43 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2552
2776
|
// unchanged state is an identical string and React bails rather than
|
|
2553
2777
|
// re-rendering the app every tick.
|
|
2554
2778
|
useEffect(function () {
|
|
2779
|
+
// A reconnect is not a fault. Action Cable drops and re-establishes its
|
|
2780
|
+
// socket routinely, and a dev server busy with a burst of saves is enough
|
|
2781
|
+
// to cause it — the cable shares the same thread pool as every request the
|
|
2782
|
+
// editor makes. Reporting the first non-connected sample made the chip
|
|
2783
|
+
// announce "Pairing off" during exactly the moments the editor was already
|
|
2784
|
+
// struggling, then clear a few seconds later, which reads as a second
|
|
2785
|
+
// failure rather than as the self-healing reconnect it actually is.
|
|
2786
|
+
//
|
|
2787
|
+
// So a transient state has to be seen twice before it is shown, with the
|
|
2788
|
+
// confirming check brought forward so a real outage still surfaces in
|
|
2789
|
+
// seconds. Hard failures are exempt: missing libraries, no Action Cable, a
|
|
2790
|
+
// server that does not advertise it, or a *rejected* handshake never heal
|
|
2791
|
+
// on their own, so they report on the first sample as before.
|
|
2792
|
+
var TRANSIENT_PROBLEMS = { connected: true };
|
|
2793
|
+
var unconfirmed = null;
|
|
2794
|
+
var recheckTimer = null;
|
|
2795
|
+
|
|
2555
2796
|
function check() {
|
|
2556
2797
|
if (typeof CollaborationService === 'undefined' ||
|
|
2557
2798
|
typeof CollaborationService.diagnostics !== 'function') return;
|
|
2558
2799
|
var d = CollaborationService.diagnostics();
|
|
2559
2800
|
// "Nobody else is here" is not a fault; everything else is.
|
|
2560
2801
|
var problem = d.firstProblem && d.firstProblem.key !== 'peers' ? d.firstProblem.key : null;
|
|
2802
|
+
var transient = !!problem && !!TRANSIENT_PROBLEMS[problem] && d.cableStatus !== 'rejected';
|
|
2803
|
+
|
|
2804
|
+
if (transient && unconfirmed !== problem) {
|
|
2805
|
+
unconfirmed = problem;
|
|
2806
|
+
clearTimeout(recheckTimer);
|
|
2807
|
+
recheckTimer = setTimeout(check, 3000);
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
unconfirmed = transient ? problem : null;
|
|
2561
2811
|
setCollabTrouble(function (prev) { return prev === problem ? prev : problem; });
|
|
2562
2812
|
}
|
|
2563
2813
|
check();
|
|
2564
2814
|
var id = setInterval(check, 10000);
|
|
2565
|
-
return function () { clearInterval(id); };
|
|
2815
|
+
return function () { clearInterval(id); clearTimeout(recheckTimer); };
|
|
2566
2816
|
}, []);
|
|
2567
2817
|
|
|
2568
2818
|
var collabPeerIds = Object.keys(collabRoster);
|
|
@@ -2689,7 +2939,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2689
2939
|
|
|
2690
2940
|
// Clear markers and skip auto-lint when RuboCop linting is disabled
|
|
2691
2941
|
if (isRubyPath(activeTab.path) && editorPrefs.rubocopLintEnabled === false) {
|
|
2692
|
-
|
|
2942
|
+
applyMarkersForTab(activeTab.id, []);
|
|
2693
2943
|
return;
|
|
2694
2944
|
}
|
|
2695
2945
|
|
|
@@ -2767,7 +3017,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2767
3017
|
return runPrettier(tab.content, editorPrefs, parserName)["catch"](function () { return null; });
|
|
2768
3018
|
};
|
|
2769
3019
|
|
|
3020
|
+
// Re-read a tab from the store after flushing TabManager's throttled content
|
|
3021
|
+
// write. `tab` here is whatever the render captured, so flushing alone is not
|
|
3022
|
+
// enough — the fresh text lands in the store, not in this object.
|
|
3023
|
+
var _freshTab = function _freshTab(paneId, tab) {
|
|
3024
|
+
TabManager.flushContent();
|
|
3025
|
+
var pane = EditorStore.getState().panes.find(function (p) { return p.id === paneId; });
|
|
3026
|
+
var live = pane && pane.tabs.find(function (t) { return t.id === tab.id; });
|
|
3027
|
+
return live || tab;
|
|
3028
|
+
};
|
|
3029
|
+
|
|
2770
3030
|
var handleSave = function handleSave(paneId, tab) {
|
|
3031
|
+
tab = _freshTab(paneId, tab);
|
|
2771
3032
|
if (editorPrefs.formatOnSave === true) {
|
|
2772
3033
|
EditorStore.setStatus("Formatting " + tab.name + "...", "info");
|
|
2773
3034
|
_formatContentForSave(tab).then(function (formatted) {
|
|
@@ -2844,21 +3105,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2844
3105
|
FileService.lintFile(tab.path, tab.content, 'javascript').then(function (res) {
|
|
2845
3106
|
var babelMarkers = (res && res.markers) || [];
|
|
2846
3107
|
if (babelMarkers.length > 0) {
|
|
2847
|
-
applyMarkersForTab(
|
|
3108
|
+
applyMarkersForTab(tab.id, babelMarkers);
|
|
2848
3109
|
EditorStore.setStatus('Saved — babel: ' + babelMarkers[0].message, 'warning');
|
|
2849
3110
|
} else {
|
|
2850
3111
|
// Clear any previous babel marker for this tab (keep others none —
|
|
2851
3112
|
// JS tabs have no rubocop markers, so replacing is safe).
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
if (existing && existing.markers && existing.markers.length) {
|
|
2855
|
-
applyMarkersForTab(paneId, tab.id, []);
|
|
2856
|
-
}
|
|
3113
|
+
// applyMarkersForTab keeps the previous map when it is already empty.
|
|
3114
|
+
applyMarkersForTab(tab.id, []);
|
|
2857
3115
|
}
|
|
2858
3116
|
})["catch"](function () {});
|
|
2859
3117
|
}
|
|
2860
3118
|
|
|
2861
|
-
|
|
3119
|
+
// The server broadcasts files_changed for this very write, and the
|
|
3120
|
+
// coalesced handler refreshes git status there — so firing here too ran
|
|
3121
|
+
// the git work twice per save, and this copy is the one a burst cannot
|
|
3122
|
+
// coalesce. fetchStatusLite escalates to the full /git_info fan-out
|
|
3123
|
+
// whenever the working tree signature moved, which saving a different
|
|
3124
|
+
// file every time does by definition, so the duplicate was the expensive
|
|
3125
|
+
// one. Without a socket no broadcast arrives, so keep it for that case.
|
|
3126
|
+
if (!_socketWillBroadcast()) GitService.fetchStatusLite({ background: true });
|
|
2862
3127
|
})["catch"](function (err) {
|
|
2863
3128
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2864
3129
|
})["finally"](function () {
|
|
@@ -2932,7 +3197,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2932
3197
|
}
|
|
2933
3198
|
|
|
2934
3199
|
var handleSaveAll = function handleSaveAll() {
|
|
2935
|
-
|
|
3200
|
+
// Flush first, then read the panes back out of the store — the render's
|
|
3201
|
+
// `state` predates the flush. See TabManager's flush contract.
|
|
3202
|
+
TabManager.flushContent();
|
|
3203
|
+
var dirtyTabs = EditorStore.getState().panes.flatMap(function (p) {
|
|
2936
3204
|
return p.tabs;
|
|
2937
3205
|
}).filter(function (t) {
|
|
2938
3206
|
// Untitled scratch tabs need a save-as prompt each — Ctrl+S them
|
|
@@ -2946,32 +3214,53 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2946
3214
|
});
|
|
2947
3215
|
EditorStore.setStatus("Saving " + dirtyTabs.length + " files...", "info");
|
|
2948
3216
|
isSavingRef.current = true;
|
|
3217
|
+
// allSettled, not all: one slow or rejected write used to abandon the
|
|
3218
|
+
// bookkeeping for every other file, so tabs whose contents were already on
|
|
3219
|
+
// disk stayed dirty and the only thing on offer was to hit Save All again.
|
|
3220
|
+
// Each file is now settled on its own result.
|
|
2949
3221
|
var promises = dirtyTabs.map(function (tab) {
|
|
2950
3222
|
return FileService.saveFile(tab.path, tab.content);
|
|
2951
3223
|
});
|
|
2952
|
-
Promise.
|
|
2953
|
-
dirtyTabs.
|
|
3224
|
+
Promise.allSettled(promises).then(function (results) {
|
|
3225
|
+
var saved = dirtyTabs.filter(function (tab, i) { return results[i].status === "fulfilled"; });
|
|
3226
|
+
// Object.create(null): these keys are file paths, and a file called
|
|
3227
|
+
// "constructor" would otherwise test as present against Object.prototype
|
|
3228
|
+
// and have its tab marked clean without ever being written.
|
|
3229
|
+
var savedPaths = Object.create(null);
|
|
3230
|
+
saved.forEach(function (tab) {
|
|
3231
|
+
savedPaths[tab.path] = tab.content;
|
|
2954
3232
|
noteLocalSave(tab.path, tab.content);
|
|
2955
3233
|
});
|
|
2956
3234
|
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
2957
3235
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3236
|
+
// Compare content, not just the path: a tab edited again while the
|
|
3237
|
+
// save was in flight is dirty against what actually reached disk.
|
|
3238
|
+
if (!(t.path in savedPaths) || t.content !== savedPaths[t.path]) return t;
|
|
2958
3239
|
return _extends({}, t, { dirty: false, cleanContent: t.content });
|
|
2959
3240
|
})
|
|
2960
3241
|
});
|
|
2961
3242
|
});
|
|
2962
3243
|
EditorStore.setState({ panes: newPanes });
|
|
2963
3244
|
// Reset AVI clean baselines for all saved files so undo past save shows dirty correctly.
|
|
2964
|
-
|
|
3245
|
+
saved.forEach(function(tab) {
|
|
2965
3246
|
var _me = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
2966
3247
|
if (_me && _me.model && !_me.model.isDisposed()) {
|
|
2967
3248
|
_me.cleanVersionId = _me.model.getAlternativeVersionId();
|
|
2968
3249
|
}
|
|
2969
3250
|
});
|
|
2970
|
-
|
|
3251
|
+
if (saved.length === dirtyTabs.length) {
|
|
3252
|
+
EditorStore.setStatus("All files saved", "success");
|
|
3253
|
+
} else {
|
|
3254
|
+
EditorStore.setStatus("Saved " + saved.length + " of " + dirtyTabs.length +
|
|
3255
|
+
" files — " + (dirtyTabs.length - saved.length) + " failed", "error");
|
|
3256
|
+
}
|
|
2971
3257
|
SearchService.invalidate();
|
|
2972
3258
|
GitService.fetchStatusLite({ background: true });
|
|
2973
|
-
})["catch"](function (
|
|
2974
|
-
|
|
3259
|
+
})["catch"](function () {
|
|
3260
|
+
// allSettled never rejects, so this only fires if the bookkeeping above
|
|
3261
|
+
// throws. Without it that would be an unhandled rejection and the status
|
|
3262
|
+
// bar would sit on "Saving N files..." forever.
|
|
3263
|
+
EditorStore.setStatus("Save All finished with errors", "error");
|
|
2975
3264
|
})["finally"](function () {
|
|
2976
3265
|
isSavingRef.current = false;
|
|
2977
3266
|
return setLoading(function (prev) {
|
|
@@ -3029,7 +3318,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3029
3318
|
return _extends({}, prev, { refreshWorkspace: true });
|
|
3030
3319
|
});
|
|
3031
3320
|
GitService.fetchStatus()["catch"](function () {});
|
|
3032
|
-
FileService.getTree().then(function (data) {
|
|
3321
|
+
FileService.getTree({ refresh: true }).then(function (data) {
|
|
3033
3322
|
setTreeData(_treeUpdater(data || []));
|
|
3034
3323
|
checkOpenTabsForExternalChanges();
|
|
3035
3324
|
EditorStore.setStatus("Workspace refreshed", "success");
|
|
@@ -3095,19 +3384,41 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3095
3384
|
return runPrettier(tab.content, prefs, parserName);
|
|
3096
3385
|
};
|
|
3097
3386
|
|
|
3387
|
+
// Markers are per-model and only the mounted editor re-lints, so a document
|
|
3388
|
+
// formatted in the background kept the diagnostics of the text it no longer
|
|
3389
|
+
// holds — the Problems panel and the status-bar tallies went on reporting
|
|
3390
|
+
// offenses at line numbers that had moved, and Format All barely moved the
|
|
3391
|
+
// counts because only the visible tab was re-checked. Dropping them says
|
|
3392
|
+
// "not known yet", which is true: the file re-lints when you open it.
|
|
3393
|
+
var discardStaleMarkers = function discardStaleMarkers(path) {
|
|
3394
|
+
if (!path || !window.monaco || !window.monaco.editor) return;
|
|
3395
|
+
var entry = window.__mbeditorModels && window.__mbeditorModels[path];
|
|
3396
|
+
if (!entry || !entry.model || entry.model.isDisposed()) return;
|
|
3397
|
+
|
|
3398
|
+
var owners = {};
|
|
3399
|
+
window.monaco.editor.getModelMarkers({ resource: entry.model.uri }).forEach(function (m) {
|
|
3400
|
+
owners[m.owner] = true;
|
|
3401
|
+
});
|
|
3402
|
+
Object.keys(owners).forEach(function (owner) {
|
|
3403
|
+
window.monaco.editor.setModelMarkers(entry.model, owner, []);
|
|
3404
|
+
});
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3098
3407
|
// Write formatted content back to a tab, dirty and unsaved — the user decides
|
|
3099
3408
|
// when to save. EditorPanel applies it through executeEdits, so undo works.
|
|
3100
3409
|
var applyFormattedContent = function applyFormattedContent(paneId, tabId, formatted) {
|
|
3410
|
+
var formattedPath = null;
|
|
3101
3411
|
EditorStore.setState({
|
|
3102
3412
|
panes: EditorStore.getState().panes.map(function (p) {
|
|
3103
3413
|
if (p.id !== paneId) return p;
|
|
3104
3414
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3415
|
+
if (t.id !== tabId) return t;
|
|
3416
|
+
formattedPath = t.path;
|
|
3417
|
+
return _extends({}, t, { content: formatted, dirty: true, externalContentVersion: (t.externalContentVersion || 0) + 1 });
|
|
3108
3418
|
}) });
|
|
3109
3419
|
})
|
|
3110
3420
|
});
|
|
3421
|
+
discardStaleMarkers(formattedPath);
|
|
3111
3422
|
};
|
|
3112
3423
|
|
|
3113
3424
|
var highlightFormatChanges = function highlightFormatChanges(before, after) {
|
|
@@ -3124,8 +3435,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3124
3435
|
var handleFormat = function handleFormat() {
|
|
3125
3436
|
if (!activeTab) return;
|
|
3126
3437
|
|
|
3127
|
-
var tab = activeTab;
|
|
3128
3438
|
var paneId = focusedPane.id;
|
|
3439
|
+
var tab = _freshTab(paneId, activeTab);
|
|
3129
3440
|
var originalContent = tab.content;
|
|
3130
3441
|
|
|
3131
3442
|
setLoading(function (prev) { return _extends({}, prev, { format: true }); });
|
|
@@ -3163,6 +3474,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3163
3474
|
// thrown, so one unparseable file cannot stop the rest. Virtual tabs (diffs,
|
|
3164
3475
|
// settings, the changelog) have no path on disk and are skipped.
|
|
3165
3476
|
var handleFormatAll = function handleFormatAll() {
|
|
3477
|
+
// See TabManager's flush contract — the panes are read straight after.
|
|
3478
|
+
TabManager.flushContent();
|
|
3166
3479
|
var targets = [];
|
|
3167
3480
|
EditorStore.getState().panes.forEach(function (p) {
|
|
3168
3481
|
p.tabs.forEach(function (t) {
|
|
@@ -3430,8 +3743,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3430
3743
|
// Update any open Monaco models whose content changed.
|
|
3431
3744
|
if (files.length && window.__mbeditorModels) {
|
|
3432
3745
|
files.forEach(function(relPath) {
|
|
3433
|
-
|
|
3434
|
-
|
|
3746
|
+
// The registry holds {model, aviBase, ...} entries, not models. This
|
|
3747
|
+
// used to call setValue/isDisposed straight on the entry, which threw
|
|
3748
|
+
// into the .catch below — so an open tab kept its pre-replace text and
|
|
3749
|
+
// could save it back over the replacement.
|
|
3750
|
+
var entry = window.__mbeditorModels[relPath];
|
|
3751
|
+
var model = entry && entry.model;
|
|
3752
|
+
if (!model || model.isDisposed()) return;
|
|
3435
3753
|
FileService.getFile(relPath).then(function(res) {
|
|
3436
3754
|
if (res && res.content != null && !model.isDisposed()) {
|
|
3437
3755
|
model.setValue(res.content);
|
|
@@ -3457,6 +3775,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3457
3775
|
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
3458
3776
|
loadMoreSearchResults();
|
|
3459
3777
|
}
|
|
3778
|
+
setSearchViewport(function (prev) {
|
|
3779
|
+
// Only the rows that moved in or out of the window matter, and those
|
|
3780
|
+
// change a row at a time. Comparing keeps a scroll gesture from
|
|
3781
|
+
// committing a render per pixel.
|
|
3782
|
+
if (prev.scrollTop === el.scrollTop && prev.height === el.clientHeight) return prev;
|
|
3783
|
+
return { scrollTop: el.scrollTop, height: el.clientHeight };
|
|
3784
|
+
});
|
|
3460
3785
|
};
|
|
3461
3786
|
|
|
3462
3787
|
var toggleGitPanel = function toggleGitPanel() {
|
|
@@ -3470,6 +3795,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3470
3795
|
setShowLogPanel(function (prev) { return !prev; });
|
|
3471
3796
|
};
|
|
3472
3797
|
|
|
3798
|
+
var toggleTestRunPanel = function toggleTestRunPanel() {
|
|
3799
|
+
setShowTestRunPanel(function (prev) { return !prev; });
|
|
3800
|
+
};
|
|
3801
|
+
|
|
3473
3802
|
var toggleProblemsPanel = function toggleProblemsPanel() {
|
|
3474
3803
|
setShowProblemsPanel(function (prev) { return !prev; });
|
|
3475
3804
|
};
|
|
@@ -3648,6 +3977,40 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3648
3977
|
});
|
|
3649
3978
|
};
|
|
3650
3979
|
|
|
3980
|
+
// Downloads go straight through /raw?download=1 — the browser's own save
|
|
3981
|
+
// flow, so nothing is buffered client-side and a 5 MB file costs no memory.
|
|
3982
|
+
// A synthetic anchor rather than location.href: navigating away from the
|
|
3983
|
+
// editor to an attachment response leaves the page in a half-unloaded state
|
|
3984
|
+
// in Safari.
|
|
3985
|
+
var handleDownloadFile = function handleDownloadFile(node) {
|
|
3986
|
+
if (!node || node.type !== 'file') return;
|
|
3987
|
+
var link = document.createElement('a');
|
|
3988
|
+
link.href = window.mbeditorBasePath() + '/raw?download=1&path=' + encodeURIComponent(node.path);
|
|
3989
|
+
link.download = node.name;
|
|
3990
|
+
link.rel = 'noopener';
|
|
3991
|
+
document.body.appendChild(link);
|
|
3992
|
+
link.click();
|
|
3993
|
+
document.body.removeChild(link);
|
|
3994
|
+
EditorStore.setStatus('Downloading ' + node.name + '...', 'info');
|
|
3995
|
+
};
|
|
3996
|
+
|
|
3997
|
+
// A file node means "upload alongside this file", so the dialog opens on its
|
|
3998
|
+
// parent — right-clicking a file to upload next to it is the common gesture.
|
|
3999
|
+
var openImportDialog = function openImportDialog(node) {
|
|
4000
|
+
var folder = '';
|
|
4001
|
+
if (node && node.type === 'folder') {
|
|
4002
|
+
folder = node.path;
|
|
4003
|
+
} else if (node && node.path) {
|
|
4004
|
+
folder = node.path.split('/').slice(0, -1).join('/');
|
|
4005
|
+
}
|
|
4006
|
+
setImportDialog({ initialFolder: folder });
|
|
4007
|
+
};
|
|
4008
|
+
|
|
4009
|
+
var confirmImportDialog = function confirmImportDialog(entries, destFolder, meta) {
|
|
4010
|
+
setImportDialog(null);
|
|
4011
|
+
handleImportFiles(entries, destFolder, meta);
|
|
4012
|
+
};
|
|
4013
|
+
|
|
3651
4014
|
var openContextMenu = function openContextMenu(e, node) {
|
|
3652
4015
|
setContextMenu({ x: e.clientX, y: e.clientY, node: node });
|
|
3653
4016
|
handleNodeSelect(node);
|
|
@@ -3675,6 +4038,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3675
4038
|
if (action === 'delete') {
|
|
3676
4039
|
handleDeletePath(node);return;
|
|
3677
4040
|
}
|
|
4041
|
+
if (action === 'download' && node) {
|
|
4042
|
+
handleDownloadFile(node);return;
|
|
4043
|
+
}
|
|
4044
|
+
if (action === 'upload') {
|
|
4045
|
+
openImportDialog(node);return;
|
|
4046
|
+
}
|
|
3678
4047
|
if (action === 'copyPath' && node) {
|
|
3679
4048
|
if (navigator.clipboard) {
|
|
3680
4049
|
navigator.clipboard.writeText(node.path)["catch"](function () {});
|
|
@@ -3823,15 +4192,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3823
4192
|
activeTabId: activePane ? activePane.activeTabId : null
|
|
3824
4193
|
});
|
|
3825
4194
|
|
|
3826
|
-
if (removedTabIds.length)
|
|
3827
|
-
setMarkers(function (prev) {
|
|
3828
|
-
var next = _extends({}, prev);
|
|
3829
|
-
removedTabIds.forEach(function (tabId) {
|
|
3830
|
-
return delete next[tabId];
|
|
3831
|
-
});
|
|
3832
|
-
return next;
|
|
3833
|
-
});
|
|
3834
|
-
}
|
|
4195
|
+
if (removedTabIds.length) forgetClosedTabs();
|
|
3835
4196
|
};
|
|
3836
4197
|
|
|
3837
4198
|
// Reveal a folder path in the explorer: switch to explorer tab and expand
|
|
@@ -3890,8 +4251,27 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3890
4251
|
FileService.createFile(path, '').then(function (res) {
|
|
3891
4252
|
var createdPath = res && res.path || path;
|
|
3892
4253
|
var createdName = createdPath.split('/').pop();
|
|
4254
|
+
// We just wrote this file and know it is empty, so opening it does not
|
|
4255
|
+
// need to ask the server what is in it. noteLocalSave records the write
|
|
4256
|
+
// for the same reason a save does: the create's own broadcast comes
|
|
4257
|
+
// straight back as a files_changed naming this path, and without this
|
|
4258
|
+
// the external-change check read the file back off disk to ask whether
|
|
4259
|
+
// it had changed since we wrote it a moment earlier.
|
|
4260
|
+
if (FileService.seedPrefetch) FileService.seedPrefetch(createdPath, '');
|
|
4261
|
+
noteLocalSave(createdPath, '');
|
|
3893
4262
|
handleNodeSelect({ path: createdPath, name: createdName, type: 'file' });
|
|
3894
4263
|
EditorStore.setStatus('Created file: ' + createdName, 'success');
|
|
4264
|
+
// The optimistic node is already in the tree and the server's
|
|
4265
|
+
// structural broadcast refreshes it for real a moment later, so
|
|
4266
|
+
// walking the workspace here as well made one create cost three full
|
|
4267
|
+
// tree walks — and every write invalidates the server's tree cache, so
|
|
4268
|
+
// each one is a fresh recursive scan of the whole checkout. Same for
|
|
4269
|
+
// git status, which the same broadcast handler already refreshes.
|
|
4270
|
+
// Without a socket neither happens on its own, so keep both for that.
|
|
4271
|
+
if (_socketWillBroadcast()) {
|
|
4272
|
+
handleSelectFile(createdPath, createdName);
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
3895
4275
|
return refreshProjectTree().then(function () {
|
|
3896
4276
|
handleSelectFile(createdPath, createdName);
|
|
3897
4277
|
GitService.fetchStatusLite({ background: true });
|
|
@@ -3914,6 +4294,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3914
4294
|
var createdPath = res && res.path || path;
|
|
3915
4295
|
handleNodeSelect({ path: createdPath, name: createdPath.split('/').pop(), type: 'folder' });
|
|
3916
4296
|
EditorStore.setStatus('Created folder: ' + createdPath, 'success');
|
|
4297
|
+
// See handleCreateConfirm's file branch — the broadcast covers both.
|
|
4298
|
+
if (_socketWillBroadcast()) return;
|
|
3917
4299
|
return refreshProjectTree().then(function () {
|
|
3918
4300
|
return GitService.fetchStatusLite({ background: true });
|
|
3919
4301
|
});
|
|
@@ -4074,6 +4456,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4074
4456
|
tabs: tabs,
|
|
4075
4457
|
activeId: activeId,
|
|
4076
4458
|
paneId: paneId,
|
|
4459
|
+
// Diagnostics live in this map, not on the tab object — writing them onto
|
|
4460
|
+
// the tab would mutate a value inside EditorStore.
|
|
4461
|
+
markers: markers,
|
|
4077
4462
|
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
4078
4463
|
onSelect: function (id) {
|
|
4079
4464
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
@@ -4275,7 +4660,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4275
4660
|
"button",
|
|
4276
4661
|
{ className: "statusbar-btn", onClick: function () {
|
|
4277
4662
|
return activeTab && handleSave(focusedPane.id, activeTab);
|
|
4278
|
-
}, disabled: loading.save || !activeTab || !activeTab.dirty, 'aria-busy': !!loading.save
|
|
4663
|
+
}, disabled: loading.save || !activeTab || !activeTab.dirty, 'aria-busy': !!loading.save,
|
|
4664
|
+
title: "Save the active file (Ctrl+S)" },
|
|
4279
4665
|
!loading.save && React.createElement("i", { className: "fas fa-save" }),
|
|
4280
4666
|
!toolbarIconOnly && !loading.save && " Save",
|
|
4281
4667
|
!loading.save && activeTab && activeTab.dirty ? " ●" : ""
|
|
@@ -4286,7 +4672,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4286
4672
|
return p.tabs;
|
|
4287
4673
|
}).some(function (t) {
|
|
4288
4674
|
return t.dirty;
|
|
4289
|
-
}), 'aria-busy': !!loading.saveAll },
|
|
4675
|
+
}), 'aria-busy': !!loading.saveAll, title: "Save every file with unsaved changes" },
|
|
4290
4676
|
!loading.saveAll && React.createElement(
|
|
4291
4677
|
"i",
|
|
4292
4678
|
{ className: "fas fa-save", style: { position: 'relative' } },
|
|
@@ -4330,7 +4716,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4330
4716
|
React.createElement("div", { className: "statusbar-sep" }),
|
|
4331
4717
|
React.createElement(
|
|
4332
4718
|
"button",
|
|
4333
|
-
{ type: "button", className: "statusbar-btn", onClick: toggleGitPanel
|
|
4719
|
+
{ type: "button", className: "statusbar-btn", onClick: toggleGitPanel,
|
|
4720
|
+
title: (showGitPanel ? "Hide" : "Show") + " the git panel (Ctrl+Shift+G)" },
|
|
4334
4721
|
React.createElement("i", { className: "fas fa-code-branch" }),
|
|
4335
4722
|
!toolbarIconOnly && " Git"
|
|
4336
4723
|
)
|
|
@@ -4545,7 +4932,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4545
4932
|
React.createElement(
|
|
4546
4933
|
"div",
|
|
4547
4934
|
{ className: "ide-body", id: "ide-body-container" },
|
|
4548
|
-
/* Activity bar — always visible,
|
|
4935
|
+
/* Activity bar — always visible, 60px wide */
|
|
4549
4936
|
!zenMode && React.createElement(
|
|
4550
4937
|
"div",
|
|
4551
4938
|
{ className: "ide-activity-bar" },
|
|
@@ -4688,6 +5075,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4688
5075
|
{
|
|
4689
5076
|
key: tab.id,
|
|
4690
5077
|
className: "tree-item " + (pane.activeTabId === tab.id && state.focusedPaneId === pane.id ? "active" : ""),
|
|
5078
|
+
// The name ellipsises in a narrow sidebar, so the row
|
|
5079
|
+
// carries the full path the way file-tree rows do.
|
|
5080
|
+
title: tab.path || tab.name,
|
|
4691
5081
|
onClick: function () {
|
|
4692
5082
|
if (tab.path && !tab.path.startsWith('mbeditor://') && tab.path !== '__settings__') {
|
|
4693
5083
|
handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
|
|
@@ -4720,7 +5110,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4720
5110
|
"div",
|
|
4721
5111
|
{ className: "tab-close", onClick: function (e) {
|
|
4722
5112
|
e.stopPropagation();requestCloseTab(pane.id, tab.id);
|
|
4723
|
-
}, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 }
|
|
5113
|
+
}, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 },
|
|
5114
|
+
// See TabBar: role="button" would pick up Pico's
|
|
5115
|
+
// button skin from the host app and square this off.
|
|
5116
|
+
title: "Close " + tab.name + (tab.dirty ? " (unsaved changes)" : "") },
|
|
4724
5117
|
React.createElement("i", { className: "fas fa-times" })
|
|
4725
5118
|
)
|
|
4726
5119
|
)
|
|
@@ -4780,6 +5173,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4780
5173
|
},
|
|
4781
5174
|
disabled: !!loading.createDir
|
|
4782
5175
|
}),
|
|
5176
|
+
React.createElement(SidebarActionButton, {
|
|
5177
|
+
title: "Upload files",
|
|
5178
|
+
iconClass: 'fas fa-upload',
|
|
5179
|
+
onClick: function () {
|
|
5180
|
+
return openImportDialog(selectedTreeNode);
|
|
5181
|
+
}
|
|
5182
|
+
}),
|
|
4783
5183
|
React.createElement(SidebarActionButton, {
|
|
4784
5184
|
title: "Rename selected",
|
|
4785
5185
|
iconClass: 'fas fa-pen',
|
|
@@ -4802,7 +5202,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4802
5202
|
)
|
|
4803
5203
|
},
|
|
4804
5204
|
React.createElement(FileTree, {
|
|
4805
|
-
items:
|
|
5205
|
+
items: fileTreeItems,
|
|
4806
5206
|
onSelect: handleSoftOpenFile,
|
|
4807
5207
|
activePath: editorPrefs.autoRevealInExplorer !== false ? (activeTab && activeTab.path) : null,
|
|
4808
5208
|
selectedPaths: selectedPaths,
|
|
@@ -4937,6 +5337,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4937
5337
|
var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
|
|
4938
5338
|
var hasAny = loadedCount > 0;
|
|
4939
5339
|
|
|
5340
|
+
// Only the rows on screen (plus a small buffer either side) are
|
|
5341
|
+
// built. Everything else is represented by the height of the
|
|
5342
|
+
// spacer, so the scrollbar and every scroll position stay exactly
|
|
5343
|
+
// as they would be for the full list.
|
|
5344
|
+
var winStart = Math.max(0, Math.floor(searchViewport.scrollTop / SEARCH_ROW_HEIGHT) - SEARCH_ROW_BUFFER);
|
|
5345
|
+
var winEnd = Math.min(loadedCount, Math.ceil((searchViewport.scrollTop + (searchViewport.height || 600)) / SEARCH_ROW_HEIGHT) + SEARCH_ROW_BUFFER);
|
|
5346
|
+
var visible = allResults.slice(winStart, winEnd);
|
|
5347
|
+
|
|
4940
5348
|
return React.createElement(
|
|
4941
5349
|
React.Fragment,
|
|
4942
5350
|
null,
|
|
@@ -4954,32 +5362,38 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4954
5362
|
"div",
|
|
4955
5363
|
{
|
|
4956
5364
|
className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
|
|
4957
|
-
ref:
|
|
5365
|
+
ref: attachSearchResults,
|
|
4958
5366
|
onScroll: handleSearchResultsScroll
|
|
4959
5367
|
},
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
5368
|
+
React.createElement(
|
|
5369
|
+
"div",
|
|
5370
|
+
{ style: { height: loadedCount * SEARCH_ROW_HEIGHT, position: 'relative' } },
|
|
5371
|
+
visible.map(function(res, vi) {
|
|
5372
|
+
var i = winStart + vi;
|
|
5373
|
+
var fileName = res.file.split('/').pop();
|
|
5374
|
+
return React.createElement(
|
|
5375
|
+
"div",
|
|
5376
|
+
{
|
|
5377
|
+
key: i,
|
|
5378
|
+
className: "search-result-item",
|
|
5379
|
+
style: { position: 'absolute', top: i * SEARCH_ROW_HEIGHT, left: 0, right: 0 },
|
|
5380
|
+
// end_col puts the cursor just past the match, which is
|
|
5381
|
+
// where you want to start typing after jumping to a hit.
|
|
5382
|
+
onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line, r.end_col || r.col); }; })(res)
|
|
5383
|
+
},
|
|
5384
|
+
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
4974
5385
|
React.createElement(
|
|
4975
|
-
"div", { className: "search-result-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5386
|
+
"div", { className: "search-result-body" },
|
|
5387
|
+
React.createElement(
|
|
5388
|
+
"div", { className: "search-result-file" },
|
|
5389
|
+
fileName,
|
|
5390
|
+
React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
|
|
5391
|
+
),
|
|
5392
|
+
React.createElement("div", { className: "search-result-text" }, res.text)
|
|
5393
|
+
)
|
|
5394
|
+
);
|
|
5395
|
+
})
|
|
5396
|
+
),
|
|
4983
5397
|
searchHasMore && React.createElement(
|
|
4984
5398
|
"div", { className: "search-loading-more" },
|
|
4985
5399
|
React.createElement("i", { className: "fas fa-spinner fa-spin" }),
|
|
@@ -5573,16 +5987,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5573
5987
|
React.createElement('option', { value: 'allDocuments' }, 'All open files')
|
|
5574
5988
|
)
|
|
5575
5989
|
),
|
|
5576
|
-
React.createElement(
|
|
5577
|
-
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Re-indent pasted code to match where it lands. Only leading whitespace changes — the formatter is not run.' },
|
|
5578
|
-
React.createElement('span', { className: 'ide-settings-label' }, 'Indent on paste'),
|
|
5579
|
-
React.createElement('input', {
|
|
5580
|
-
type: 'checkbox',
|
|
5581
|
-
className: 'ide-settings-checkbox',
|
|
5582
|
-
checked: editorPrefs.indentOnPaste !== false,
|
|
5583
|
-
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { indentOnPaste: v }); }); }
|
|
5584
|
-
})
|
|
5585
|
-
),
|
|
5586
5990
|
React.createElement(
|
|
5587
5991
|
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Re-indent and auto-close blocks as you type (e.g. after pressing Enter inside {})' },
|
|
5588
5992
|
React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
|
|
@@ -5767,6 +6171,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5767
6171
|
})
|
|
5768
6172
|
),
|
|
5769
6173
|
|
|
6174
|
+
React.createElement(
|
|
6175
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show the verb and path that route to each controller action after its def line, and mark public actions nothing routes to' },
|
|
6176
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Controller route hints'),
|
|
6177
|
+
React.createElement('input', {
|
|
6178
|
+
type: 'checkbox',
|
|
6179
|
+
className: 'ide-settings-checkbox',
|
|
6180
|
+
checked: editorPrefs.routeHints !== false,
|
|
6181
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { routeHints: v }); }); }
|
|
6182
|
+
})
|
|
6183
|
+
),
|
|
6184
|
+
|
|
5770
6185
|
/* ── RuboCop ─────────────────────────────────── */
|
|
5771
6186
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
|
|
5772
6187
|
React.createElement(
|
|
@@ -5798,6 +6213,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5798
6213
|
{
|
|
5799
6214
|
className: 'ide-settings-reset-btn',
|
|
5800
6215
|
type: 'button',
|
|
6216
|
+
title: 'Restore every editor preference on this page to its default',
|
|
5801
6217
|
onClick: function() { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); }
|
|
5802
6218
|
},
|
|
5803
6219
|
React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
|
|
@@ -6019,6 +6435,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6019
6435
|
onOpenFile: function (path, line, col) {
|
|
6020
6436
|
handleSelectFile(path, path.split('/').pop(), line, col);
|
|
6021
6437
|
}
|
|
6438
|
+
}),
|
|
6439
|
+
showTestRunPanel && !zenMode && React.createElement(window.TestRunPanel, {
|
|
6440
|
+
onClose: function () { setShowTestRunPanel(false); },
|
|
6441
|
+
onOpenFile: function (path, line, col) {
|
|
6442
|
+
handleSelectFile(path, path.split('/').pop(), line, col);
|
|
6443
|
+
},
|
|
6444
|
+
// A suite result spans many files, so there is no single testPanelFile
|
|
6445
|
+
// for it — each entry carries its own, and EditorPanel matches per
|
|
6446
|
+
// entry. Clearing it here keeps the per-file modal from labelling a
|
|
6447
|
+
// suite result with a file it did not come from.
|
|
6448
|
+
onResult: function (res) {
|
|
6449
|
+
setTestResult(res);
|
|
6450
|
+
setTestPanelFile(null);
|
|
6451
|
+
}
|
|
6022
6452
|
})
|
|
6023
6453
|
),
|
|
6024
6454
|
|
|
@@ -6085,6 +6515,19 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6085
6515
|
}),
|
|
6086
6516
|
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
|
|
6087
6517
|
),
|
|
6518
|
+
// Gated on the same probe as the per-file Test button: a project with no
|
|
6519
|
+
// test directory has nothing for this to run.
|
|
6520
|
+
testAvailable && React.createElement(
|
|
6521
|
+
"button",
|
|
6522
|
+
{
|
|
6523
|
+
type: "button",
|
|
6524
|
+
className: "statusbar-btn statusbar-testrun" + (showTestRunPanel ? " active" : ""),
|
|
6525
|
+
onClick: toggleTestRunPanel,
|
|
6526
|
+
title: "Run the whole test suite"
|
|
6527
|
+
},
|
|
6528
|
+
React.createElement("i", { className: "fas fa-flask" }),
|
|
6529
|
+
React.createElement("span", null, " Tests")
|
|
6530
|
+
),
|
|
6088
6531
|
// ruby-lsp indicator. Hidden entirely when ruby-lsp was never available
|
|
6089
6532
|
// and nothing has gone wrong — a permanent "off" badge in a project with
|
|
6090
6533
|
// no Ruby is noise. A healthy server gets a quiet icon; a degraded one
|
|
@@ -6424,6 +6867,23 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6424
6867
|
" Delete"
|
|
6425
6868
|
),
|
|
6426
6869
|
React.createElement("div", { className: "context-menu-divider" }),
|
|
6870
|
+
contextMenu.node && contextMenu.node.type === 'file' && React.createElement(
|
|
6871
|
+
"div",
|
|
6872
|
+
{ className: "context-menu-item", onClick: function () {
|
|
6873
|
+
return handleContextMenuAction('download');
|
|
6874
|
+
} },
|
|
6875
|
+
React.createElement("i", { className: "fas fa-download context-menu-icon" }),
|
|
6876
|
+
" Download"
|
|
6877
|
+
),
|
|
6878
|
+
React.createElement(
|
|
6879
|
+
"div",
|
|
6880
|
+
{ className: "context-menu-item", onClick: function () {
|
|
6881
|
+
return handleContextMenuAction('upload');
|
|
6882
|
+
} },
|
|
6883
|
+
React.createElement("i", { className: "fas fa-upload context-menu-icon" }),
|
|
6884
|
+
" Upload Files Here..."
|
|
6885
|
+
),
|
|
6886
|
+
React.createElement("div", { className: "context-menu-divider" }),
|
|
6427
6887
|
React.createElement(
|
|
6428
6888
|
"div",
|
|
6429
6889
|
{ className: "context-menu-item", onClick: function () {
|
|
@@ -6499,6 +6959,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6499
6959
|
)
|
|
6500
6960
|
),
|
|
6501
6961
|
|
|
6962
|
+
/* ── Upload dialog ─────────────────────────────────────────────────── */
|
|
6963
|
+
importDialog && React.createElement(ImportDialog, {
|
|
6964
|
+
initialFolder: importDialog.initialFolder,
|
|
6965
|
+
docs: SearchService.allDocs(),
|
|
6966
|
+
onCancel: function () { setImportDialog(null); },
|
|
6967
|
+
onImport: confirmImportDialog
|
|
6968
|
+
}),
|
|
6969
|
+
|
|
6502
6970
|
/* ── Import conflict modal ─────────────────────────────────────────── */
|
|
6503
6971
|
importConflict && React.createElement(ImportConflictModal, {
|
|
6504
6972
|
conflicts: importConflict.result.conflicts,
|