mbeditor 0.12.8 → 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 +207 -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 +135 -40
- 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 +629 -137
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
- 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 +444 -116
- 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 +302 -55
- 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 +75 -10
- 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
|
-
formatOnPaste: 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
|
};
|
|
@@ -856,10 +937,33 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
856
937
|
});
|
|
857
938
|
};
|
|
858
939
|
|
|
940
|
+
// Every structural mutation (create, delete, rename, import) funnels through
|
|
941
|
+
// here, so this is where the project-search cache has to be dropped. Saves
|
|
942
|
+
// invalidated it at their own call sites, which is why editing a file
|
|
943
|
+
// updated the results but adding or deleting one never did — the stale
|
|
944
|
+
// cached page was served for the same query. The per-path delta refresh on
|
|
945
|
+
// the files_changed push can't cover it either: it re-scans named files,
|
|
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
|
+
|
|
859
958
|
var refreshProjectTree = function refreshProjectTree() {
|
|
860
959
|
return FileService.getTree().then(function (data) {
|
|
861
|
-
|
|
862
|
-
|
|
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 || []));
|
|
963
|
+
SearchService.invalidate();
|
|
964
|
+
if (searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
965
|
+
_debouncedSearch(searchQueryRef.current);
|
|
966
|
+
}
|
|
863
967
|
return data || [];
|
|
864
968
|
})["catch"](function (err) {
|
|
865
969
|
EditorStore.setStatus("Failed to refresh files: " + (err && err.message || "Unknown error"), "error");
|
|
@@ -977,15 +1081,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
977
1081
|
});
|
|
978
1082
|
};
|
|
979
1083
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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) {
|
|
988
1093
|
setMarkers(function (prev) {
|
|
1094
|
+
var cur = prev[tabId];
|
|
1095
|
+
if (cur && cur.length === 0 && nextMarkers.length === 0) return prev;
|
|
989
1096
|
return _extends({}, prev, _defineProperty({}, tabId, nextMarkers));
|
|
990
1097
|
});
|
|
991
1098
|
};
|
|
@@ -995,6 +1102,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
995
1102
|
|
|
996
1103
|
if (!tab || (!isRubyPath(tab.path) && !tab.path.endsWith('.haml'))) return Promise.resolve(null);
|
|
997
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
|
+
|
|
998
1119
|
if (options.showLoading) {
|
|
999
1120
|
setLoading(function (prev) {
|
|
1000
1121
|
return _extends({}, prev, { lint: true });
|
|
@@ -1026,7 +1147,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1026
1147
|
|
|
1027
1148
|
return lintRequest.then(function (res) {
|
|
1028
1149
|
var nextMarkers = res.markers || [];
|
|
1029
|
-
applyMarkersForTab(
|
|
1150
|
+
applyMarkersForTab(tab.id, nextMarkers);
|
|
1030
1151
|
|
|
1031
1152
|
if (options.showStatus) {
|
|
1032
1153
|
var count = res.summary && res.summary.offense_count || 0;
|
|
@@ -1064,16 +1185,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1064
1185
|
if (parserName && window.prettier && window.prettierPlugins) {
|
|
1065
1186
|
var prefs = EditorStore.getState().editorPrefs || DEFAULT_EDITOR_PREFS;
|
|
1066
1187
|
window.prettier.format(tab.content, prettierOptions(prefs, parserName)).then(function () {
|
|
1067
|
-
|
|
1068
|
-
return p.id === paneId;
|
|
1069
|
-
});
|
|
1070
|
-
var current = currentPane ? currentPane.tabs.find(function (t) {
|
|
1071
|
-
return t.id === tab.id;
|
|
1072
|
-
}) : null;
|
|
1073
|
-
if (current) current.markers = [];
|
|
1074
|
-
setMarkers(function (prev) {
|
|
1075
|
-
return _extends({}, prev, _defineProperty({}, tab.id, []));
|
|
1076
|
-
});
|
|
1188
|
+
applyMarkersForTab(tab.id, []);
|
|
1077
1189
|
})["catch"](function (err) {
|
|
1078
1190
|
var newMarkers = [];
|
|
1079
1191
|
if (err && err.loc) {
|
|
@@ -1090,16 +1202,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1090
1202
|
endCol: endLoc ? endLoc.column : loc.column + 1
|
|
1091
1203
|
});
|
|
1092
1204
|
}
|
|
1093
|
-
|
|
1094
|
-
return p.id === paneId;
|
|
1095
|
-
});
|
|
1096
|
-
var current = currentPane ? currentPane.tabs.find(function (t) {
|
|
1097
|
-
return t.id === tab.id;
|
|
1098
|
-
}) : null;
|
|
1099
|
-
if (current) current.markers = newMarkers;
|
|
1100
|
-
setMarkers(function (prev) {
|
|
1101
|
-
return _extends({}, prev, _defineProperty({}, tab.id, newMarkers));
|
|
1102
|
-
});
|
|
1205
|
+
applyMarkersForTab(tab.id, newMarkers);
|
|
1103
1206
|
});
|
|
1104
1207
|
}
|
|
1105
1208
|
}, 600)).current;
|
|
@@ -1157,6 +1260,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1157
1260
|
if (workspace && typeof workspace.redmineEnabled === 'boolean') {
|
|
1158
1261
|
setRedmineEnabled(workspace.redmineEnabled);
|
|
1159
1262
|
}
|
|
1263
|
+
if (workspace && (workspace.testTimeout || workspace.testAllTimeout)) {
|
|
1264
|
+
FileService.setTestTimeouts({
|
|
1265
|
+
test: workspace.testTimeout,
|
|
1266
|
+
testAll: workspace.testAllTimeout
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1160
1269
|
if (workspace && typeof workspace.testAvailable === 'boolean') {
|
|
1161
1270
|
setTestAvailable(workspace.testAvailable);
|
|
1162
1271
|
}
|
|
@@ -1663,25 +1772,118 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1663
1772
|
});
|
|
1664
1773
|
});
|
|
1665
1774
|
}, Promise.resolve());
|
|
1666
|
-
|
|
1775
|
+
// 250ms, not 2s. The window only exists to coalesce the paths from a
|
|
1776
|
+
// burst of saves; anything longer is dead time the user spends looking at
|
|
1777
|
+
// stale search rows, and the server-side result cache was already dropped
|
|
1778
|
+
// by the same broadcast, so waiting buys nothing.
|
|
1779
|
+
}, 250)).current;
|
|
1667
1780
|
|
|
1668
1781
|
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1669
1782
|
// and git status immediately (same work as the 10s poll below does).
|
|
1670
1783
|
useEffect(function () {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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;
|
|
1810
|
+
// The cheap /git_status probe, not the full /git_info fan-out. This
|
|
1811
|
+
// fires on every save, and the fan-out is the most expensive request
|
|
1812
|
+
// the editor makes — on a dev server with a handful of threads it
|
|
1813
|
+
// queues the tree and search requests behind itself, which is what made
|
|
1814
|
+
// search look like it was waiting for git. fetchStatusLite patches the
|
|
1815
|
+
// branch and file list immediately and escalates to the fan-out on its
|
|
1816
|
+
// own when the branch actually changed.
|
|
1817
|
+
GitService.fetchStatusLite({ background: true })["catch"](function () {});
|
|
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); });
|
|
1680
1838
|
_flushSearchRefresh();
|
|
1681
1839
|
}
|
|
1682
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
|
+
|
|
1683
1863
|
WebSocketService.onFilesChanged(handleFilesChanged);
|
|
1684
|
-
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); };
|
|
1685
1887
|
}, []);
|
|
1686
1888
|
|
|
1687
1889
|
// WebSocket push — when a peer saves a collaboratively-bound file, the CRDT has
|
|
@@ -1904,7 +2106,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1904
2106
|
if (timer) clearTimeout(timer);
|
|
1905
2107
|
timer = setTimeout(function () {
|
|
1906
2108
|
timer = null;
|
|
1907
|
-
|
|
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
|
+
});
|
|
1908
2116
|
}, 250);
|
|
1909
2117
|
};
|
|
1910
2118
|
|
|
@@ -1946,6 +2154,33 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1946
2154
|
handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
|
|
1947
2155
|
};
|
|
1948
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
|
+
|
|
1949
2184
|
var requestCloseTab = function requestCloseTab(paneId, id) {
|
|
1950
2185
|
var pane = state.panes.find(function (p) {
|
|
1951
2186
|
return p.id === paneId;
|
|
@@ -1965,6 +2200,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1965
2200
|
});
|
|
1966
2201
|
})
|
|
1967
2202
|
});
|
|
2203
|
+
forgetClosedTabs();
|
|
1968
2204
|
}
|
|
1969
2205
|
};
|
|
1970
2206
|
|
|
@@ -2004,7 +2240,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2004
2240
|
noteLocalSave(tab.path, tab.content);
|
|
2005
2241
|
EditorStore.setStatus("Saved", "success");
|
|
2006
2242
|
SearchService.invalidate();
|
|
2007
|
-
GitService.
|
|
2243
|
+
GitService.fetchStatusLite({ background: true });
|
|
2008
2244
|
// Reset the AVI clean baseline so undo past this save point shows dirty correctly.
|
|
2009
2245
|
var _closeEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
2010
2246
|
if (_closeEntry && _closeEntry.model && !_closeEntry.model.isDisposed()) {
|
|
@@ -2018,6 +2254,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2018
2254
|
});
|
|
2019
2255
|
})
|
|
2020
2256
|
});
|
|
2257
|
+
forgetClosedTabs();
|
|
2021
2258
|
})["catch"](function (err) {
|
|
2022
2259
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2023
2260
|
})["finally"](function () {
|
|
@@ -2037,6 +2274,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2037
2274
|
});
|
|
2038
2275
|
})
|
|
2039
2276
|
});
|
|
2277
|
+
forgetClosedTabs();
|
|
2040
2278
|
setClosingTabId(null);
|
|
2041
2279
|
setClosingPaneId(null);
|
|
2042
2280
|
}
|
|
@@ -2061,6 +2299,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2061
2299
|
if (!confirmBulkClose(allTabs, "all editors")) return;
|
|
2062
2300
|
|
|
2063
2301
|
TabManager.closeAllTabs();
|
|
2302
|
+
forgetClosedTabs();
|
|
2064
2303
|
setClosingTabId(null);
|
|
2065
2304
|
setClosingPaneId(null);
|
|
2066
2305
|
EditorStore.setStatus("Closed " + allTabs.length + " editor" + (allTabs.length === 1 ? "" : "s"), "info");
|
|
@@ -2074,6 +2313,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2074
2313
|
if (!confirmBulkClose(pane.tabs, "all editors in Group " + paneId)) return;
|
|
2075
2314
|
|
|
2076
2315
|
TabManager.closeAllTabsInPane(paneId);
|
|
2316
|
+
forgetClosedTabs();
|
|
2077
2317
|
setClosingTabId(null);
|
|
2078
2318
|
setClosingPaneId(null);
|
|
2079
2319
|
EditorStore.setStatus("Closed " + pane.tabs.length + " editor" + (pane.tabs.length === 1 ? "" : "s") + " in Group " + paneId, "info");
|
|
@@ -2086,6 +2326,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2086
2326
|
if (others.length === 0) return;
|
|
2087
2327
|
if (!confirmBulkClose(others, "other editors")) return;
|
|
2088
2328
|
TabManager.closeOtherTabsInPane(paneId, keepId);
|
|
2329
|
+
forgetClosedTabs();
|
|
2089
2330
|
EditorStore.setStatus("Closed " + others.length + " editor" + (others.length === 1 ? "" : "s"), "info");
|
|
2090
2331
|
};
|
|
2091
2332
|
|
|
@@ -2095,6 +2336,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2095
2336
|
var saved = pane.tabs.filter(function (t) { return !t.dirty; });
|
|
2096
2337
|
if (saved.length === 0) return;
|
|
2097
2338
|
TabManager.closeSavedTabsInPane(paneId);
|
|
2339
|
+
forgetClosedTabs();
|
|
2098
2340
|
EditorStore.setStatus("Closed " + saved.length + " saved editor" + (saved.length === 1 ? "" : "s"), "info");
|
|
2099
2341
|
};
|
|
2100
2342
|
|
|
@@ -2109,9 +2351,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2109
2351
|
return FileService.saveFile(newPath, tab.content).then(function () {
|
|
2110
2352
|
noteLocalSave(newPath, tab.content);
|
|
2111
2353
|
SearchService.invalidate();
|
|
2112
|
-
GitService.
|
|
2354
|
+
GitService.fetchStatusLite({ background: true });
|
|
2113
2355
|
FileService.getTree().then(function (data) { setTreeData(_treeUpdater(data || [])); })["catch"](function () {});
|
|
2114
2356
|
TabManager.closeTab(paneId, tab.id);
|
|
2357
|
+
forgetClosedTabs();
|
|
2115
2358
|
if (!(opts && opts.close)) {
|
|
2116
2359
|
TabManager.openTab(newPath, newPath.split('/').pop(), null, paneId);
|
|
2117
2360
|
}
|
|
@@ -2274,7 +2517,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2274
2517
|
var RAILS_MAX_RESOURCES = 10;
|
|
2275
2518
|
|
|
2276
2519
|
// Map resource label → representative path (capped at RAILS_MAX_RESOURCES, focused pane first)
|
|
2277
|
-
|
|
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() {
|
|
2278
2524
|
var deps = {};
|
|
2279
2525
|
var panesOrdered = state.panes.slice().sort(function(a, b) {
|
|
2280
2526
|
return a.id === state.focusedPaneId ? -1 : b.id === state.focusedPaneId ? 1 : 0;
|
|
@@ -2291,10 +2537,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2291
2537
|
});
|
|
2292
2538
|
});
|
|
2293
2539
|
return deps;
|
|
2294
|
-
})
|
|
2540
|
+
}, [state.panes, state.focusedPaneId, customPaths]);
|
|
2295
2541
|
var railsResourceDepStr = Object.keys(railsResourceDeps).sort().join('|');
|
|
2296
2542
|
|
|
2297
|
-
var railsOverflow = (function() {
|
|
2543
|
+
var railsOverflow = useMemo(function() {
|
|
2298
2544
|
var all = {};
|
|
2299
2545
|
state.panes.forEach(function(p) {
|
|
2300
2546
|
p.tabs.forEach(function(t) {
|
|
@@ -2304,9 +2550,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2304
2550
|
});
|
|
2305
2551
|
});
|
|
2306
2552
|
return Math.max(0, Object.keys(all).length - Object.keys(railsResourceDeps).length);
|
|
2307
|
-
})
|
|
2553
|
+
}, [state.panes, railsResourceDeps, customPaths]);
|
|
2308
2554
|
|
|
2309
|
-
var dirtyPaths = (function() {
|
|
2555
|
+
var dirtyPaths = useMemo(function() {
|
|
2310
2556
|
var set = {};
|
|
2311
2557
|
state.panes.forEach(function(p) {
|
|
2312
2558
|
p.tabs.forEach(function(t) {
|
|
@@ -2314,7 +2560,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2314
2560
|
});
|
|
2315
2561
|
});
|
|
2316
2562
|
return set;
|
|
2317
|
-
})
|
|
2563
|
+
}, [state.panes]);
|
|
2318
2564
|
|
|
2319
2565
|
useEffect(function() {
|
|
2320
2566
|
if (activeSidebarTab !== 'rails') return;
|
|
@@ -2530,17 +2776,43 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2530
2776
|
// unchanged state is an identical string and React bails rather than
|
|
2531
2777
|
// re-rendering the app every tick.
|
|
2532
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
|
+
|
|
2533
2796
|
function check() {
|
|
2534
2797
|
if (typeof CollaborationService === 'undefined' ||
|
|
2535
2798
|
typeof CollaborationService.diagnostics !== 'function') return;
|
|
2536
2799
|
var d = CollaborationService.diagnostics();
|
|
2537
2800
|
// "Nobody else is here" is not a fault; everything else is.
|
|
2538
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;
|
|
2539
2811
|
setCollabTrouble(function (prev) { return prev === problem ? prev : problem; });
|
|
2540
2812
|
}
|
|
2541
2813
|
check();
|
|
2542
2814
|
var id = setInterval(check, 10000);
|
|
2543
|
-
return function () { clearInterval(id); };
|
|
2815
|
+
return function () { clearInterval(id); clearTimeout(recheckTimer); };
|
|
2544
2816
|
}, []);
|
|
2545
2817
|
|
|
2546
2818
|
var collabPeerIds = Object.keys(collabRoster);
|
|
@@ -2667,7 +2939,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2667
2939
|
|
|
2668
2940
|
// Clear markers and skip auto-lint when RuboCop linting is disabled
|
|
2669
2941
|
if (isRubyPath(activeTab.path) && editorPrefs.rubocopLintEnabled === false) {
|
|
2670
|
-
|
|
2942
|
+
applyMarkersForTab(activeTab.id, []);
|
|
2671
2943
|
return;
|
|
2672
2944
|
}
|
|
2673
2945
|
|
|
@@ -2745,7 +3017,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2745
3017
|
return runPrettier(tab.content, editorPrefs, parserName)["catch"](function () { return null; });
|
|
2746
3018
|
};
|
|
2747
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
|
+
|
|
2748
3030
|
var handleSave = function handleSave(paneId, tab) {
|
|
3031
|
+
tab = _freshTab(paneId, tab);
|
|
2749
3032
|
if (editorPrefs.formatOnSave === true) {
|
|
2750
3033
|
EditorStore.setStatus("Formatting " + tab.name + "...", "info");
|
|
2751
3034
|
_formatContentForSave(tab).then(function (formatted) {
|
|
@@ -2822,21 +3105,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2822
3105
|
FileService.lintFile(tab.path, tab.content, 'javascript').then(function (res) {
|
|
2823
3106
|
var babelMarkers = (res && res.markers) || [];
|
|
2824
3107
|
if (babelMarkers.length > 0) {
|
|
2825
|
-
applyMarkersForTab(
|
|
3108
|
+
applyMarkersForTab(tab.id, babelMarkers);
|
|
2826
3109
|
EditorStore.setStatus('Saved — babel: ' + babelMarkers[0].message, 'warning');
|
|
2827
3110
|
} else {
|
|
2828
3111
|
// Clear any previous babel marker for this tab (keep others none —
|
|
2829
3112
|
// JS tabs have no rubocop markers, so replacing is safe).
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
if (existing && existing.markers && existing.markers.length) {
|
|
2833
|
-
applyMarkersForTab(paneId, tab.id, []);
|
|
2834
|
-
}
|
|
3113
|
+
// applyMarkersForTab keeps the previous map when it is already empty.
|
|
3114
|
+
applyMarkersForTab(tab.id, []);
|
|
2835
3115
|
}
|
|
2836
3116
|
})["catch"](function () {});
|
|
2837
3117
|
}
|
|
2838
3118
|
|
|
2839
|
-
|
|
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 });
|
|
2840
3127
|
})["catch"](function (err) {
|
|
2841
3128
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2842
3129
|
})["finally"](function () {
|
|
@@ -2910,7 +3197,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2910
3197
|
}
|
|
2911
3198
|
|
|
2912
3199
|
var handleSaveAll = function handleSaveAll() {
|
|
2913
|
-
|
|
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) {
|
|
2914
3204
|
return p.tabs;
|
|
2915
3205
|
}).filter(function (t) {
|
|
2916
3206
|
// Untitled scratch tabs need a save-as prompt each — Ctrl+S them
|
|
@@ -2924,32 +3214,53 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2924
3214
|
});
|
|
2925
3215
|
EditorStore.setStatus("Saving " + dirtyTabs.length + " files...", "info");
|
|
2926
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.
|
|
2927
3221
|
var promises = dirtyTabs.map(function (tab) {
|
|
2928
3222
|
return FileService.saveFile(tab.path, tab.content);
|
|
2929
3223
|
});
|
|
2930
|
-
Promise.
|
|
2931
|
-
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;
|
|
2932
3232
|
noteLocalSave(tab.path, tab.content);
|
|
2933
3233
|
});
|
|
2934
3234
|
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
2935
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;
|
|
2936
3239
|
return _extends({}, t, { dirty: false, cleanContent: t.content });
|
|
2937
3240
|
})
|
|
2938
3241
|
});
|
|
2939
3242
|
});
|
|
2940
3243
|
EditorStore.setState({ panes: newPanes });
|
|
2941
3244
|
// Reset AVI clean baselines for all saved files so undo past save shows dirty correctly.
|
|
2942
|
-
|
|
3245
|
+
saved.forEach(function(tab) {
|
|
2943
3246
|
var _me = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
2944
3247
|
if (_me && _me.model && !_me.model.isDisposed()) {
|
|
2945
3248
|
_me.cleanVersionId = _me.model.getAlternativeVersionId();
|
|
2946
3249
|
}
|
|
2947
3250
|
});
|
|
2948
|
-
|
|
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
|
+
}
|
|
2949
3257
|
SearchService.invalidate();
|
|
2950
|
-
GitService.
|
|
2951
|
-
})["catch"](function (
|
|
2952
|
-
|
|
3258
|
+
GitService.fetchStatusLite({ background: true });
|
|
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");
|
|
2953
3264
|
})["finally"](function () {
|
|
2954
3265
|
isSavingRef.current = false;
|
|
2955
3266
|
return setLoading(function (prev) {
|
|
@@ -3007,7 +3318,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3007
3318
|
return _extends({}, prev, { refreshWorkspace: true });
|
|
3008
3319
|
});
|
|
3009
3320
|
GitService.fetchStatus()["catch"](function () {});
|
|
3010
|
-
FileService.getTree().then(function (data) {
|
|
3321
|
+
FileService.getTree({ refresh: true }).then(function (data) {
|
|
3011
3322
|
setTreeData(_treeUpdater(data || []));
|
|
3012
3323
|
checkOpenTabsForExternalChanges();
|
|
3013
3324
|
EditorStore.setStatus("Workspace refreshed", "success");
|
|
@@ -3073,19 +3384,41 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3073
3384
|
return runPrettier(tab.content, prefs, parserName);
|
|
3074
3385
|
};
|
|
3075
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
|
+
|
|
3076
3407
|
// Write formatted content back to a tab, dirty and unsaved — the user decides
|
|
3077
3408
|
// when to save. EditorPanel applies it through executeEdits, so undo works.
|
|
3078
3409
|
var applyFormattedContent = function applyFormattedContent(paneId, tabId, formatted) {
|
|
3410
|
+
var formattedPath = null;
|
|
3079
3411
|
EditorStore.setState({
|
|
3080
3412
|
panes: EditorStore.getState().panes.map(function (p) {
|
|
3081
3413
|
if (p.id !== paneId) return p;
|
|
3082
3414
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3415
|
+
if (t.id !== tabId) return t;
|
|
3416
|
+
formattedPath = t.path;
|
|
3417
|
+
return _extends({}, t, { content: formatted, dirty: true, externalContentVersion: (t.externalContentVersion || 0) + 1 });
|
|
3086
3418
|
}) });
|
|
3087
3419
|
})
|
|
3088
3420
|
});
|
|
3421
|
+
discardStaleMarkers(formattedPath);
|
|
3089
3422
|
};
|
|
3090
3423
|
|
|
3091
3424
|
var highlightFormatChanges = function highlightFormatChanges(before, after) {
|
|
@@ -3102,8 +3435,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3102
3435
|
var handleFormat = function handleFormat() {
|
|
3103
3436
|
if (!activeTab) return;
|
|
3104
3437
|
|
|
3105
|
-
var tab = activeTab;
|
|
3106
3438
|
var paneId = focusedPane.id;
|
|
3439
|
+
var tab = _freshTab(paneId, activeTab);
|
|
3107
3440
|
var originalContent = tab.content;
|
|
3108
3441
|
|
|
3109
3442
|
setLoading(function (prev) { return _extends({}, prev, { format: true }); });
|
|
@@ -3141,6 +3474,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3141
3474
|
// thrown, so one unparseable file cannot stop the rest. Virtual tabs (diffs,
|
|
3142
3475
|
// settings, the changelog) have no path on disk and are skipped.
|
|
3143
3476
|
var handleFormatAll = function handleFormatAll() {
|
|
3477
|
+
// See TabManager's flush contract — the panes are read straight after.
|
|
3478
|
+
TabManager.flushContent();
|
|
3144
3479
|
var targets = [];
|
|
3145
3480
|
EditorStore.getState().panes.forEach(function (p) {
|
|
3146
3481
|
p.tabs.forEach(function (t) {
|
|
@@ -3408,8 +3743,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3408
3743
|
// Update any open Monaco models whose content changed.
|
|
3409
3744
|
if (files.length && window.__mbeditorModels) {
|
|
3410
3745
|
files.forEach(function(relPath) {
|
|
3411
|
-
|
|
3412
|
-
|
|
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;
|
|
3413
3753
|
FileService.getFile(relPath).then(function(res) {
|
|
3414
3754
|
if (res && res.content != null && !model.isDisposed()) {
|
|
3415
3755
|
model.setValue(res.content);
|
|
@@ -3435,6 +3775,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3435
3775
|
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
3436
3776
|
loadMoreSearchResults();
|
|
3437
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
|
+
});
|
|
3438
3785
|
};
|
|
3439
3786
|
|
|
3440
3787
|
var toggleGitPanel = function toggleGitPanel() {
|
|
@@ -3448,6 +3795,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3448
3795
|
setShowLogPanel(function (prev) { return !prev; });
|
|
3449
3796
|
};
|
|
3450
3797
|
|
|
3798
|
+
var toggleTestRunPanel = function toggleTestRunPanel() {
|
|
3799
|
+
setShowTestRunPanel(function (prev) { return !prev; });
|
|
3800
|
+
};
|
|
3801
|
+
|
|
3451
3802
|
var toggleProblemsPanel = function toggleProblemsPanel() {
|
|
3452
3803
|
setShowProblemsPanel(function (prev) { return !prev; });
|
|
3453
3804
|
};
|
|
@@ -3626,6 +3977,40 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3626
3977
|
});
|
|
3627
3978
|
};
|
|
3628
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
|
+
|
|
3629
4014
|
var openContextMenu = function openContextMenu(e, node) {
|
|
3630
4015
|
setContextMenu({ x: e.clientX, y: e.clientY, node: node });
|
|
3631
4016
|
handleNodeSelect(node);
|
|
@@ -3653,6 +4038,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3653
4038
|
if (action === 'delete') {
|
|
3654
4039
|
handleDeletePath(node);return;
|
|
3655
4040
|
}
|
|
4041
|
+
if (action === 'download' && node) {
|
|
4042
|
+
handleDownloadFile(node);return;
|
|
4043
|
+
}
|
|
4044
|
+
if (action === 'upload') {
|
|
4045
|
+
openImportDialog(node);return;
|
|
4046
|
+
}
|
|
3656
4047
|
if (action === 'copyPath' && node) {
|
|
3657
4048
|
if (navigator.clipboard) {
|
|
3658
4049
|
navigator.clipboard.writeText(node.path)["catch"](function () {});
|
|
@@ -3801,15 +4192,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3801
4192
|
activeTabId: activePane ? activePane.activeTabId : null
|
|
3802
4193
|
});
|
|
3803
4194
|
|
|
3804
|
-
if (removedTabIds.length)
|
|
3805
|
-
setMarkers(function (prev) {
|
|
3806
|
-
var next = _extends({}, prev);
|
|
3807
|
-
removedTabIds.forEach(function (tabId) {
|
|
3808
|
-
return delete next[tabId];
|
|
3809
|
-
});
|
|
3810
|
-
return next;
|
|
3811
|
-
});
|
|
3812
|
-
}
|
|
4195
|
+
if (removedTabIds.length) forgetClosedTabs();
|
|
3813
4196
|
};
|
|
3814
4197
|
|
|
3815
4198
|
// Reveal a folder path in the explorer: switch to explorer tab and expand
|
|
@@ -3868,11 +4251,30 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3868
4251
|
FileService.createFile(path, '').then(function (res) {
|
|
3869
4252
|
var createdPath = res && res.path || path;
|
|
3870
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, '');
|
|
3871
4262
|
handleNodeSelect({ path: createdPath, name: createdName, type: 'file' });
|
|
3872
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
|
+
}
|
|
3873
4275
|
return refreshProjectTree().then(function () {
|
|
3874
4276
|
handleSelectFile(createdPath, createdName);
|
|
3875
|
-
GitService.
|
|
4277
|
+
GitService.fetchStatusLite({ background: true });
|
|
3876
4278
|
});
|
|
3877
4279
|
})["catch"](function (err) {
|
|
3878
4280
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3892,8 +4294,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3892
4294
|
var createdPath = res && res.path || path;
|
|
3893
4295
|
handleNodeSelect({ path: createdPath, name: createdPath.split('/').pop(), type: 'folder' });
|
|
3894
4296
|
EditorStore.setStatus('Created folder: ' + createdPath, 'success');
|
|
4297
|
+
// See handleCreateConfirm's file branch — the broadcast covers both.
|
|
4298
|
+
if (_socketWillBroadcast()) return;
|
|
3895
4299
|
return refreshProjectTree().then(function () {
|
|
3896
|
-
return GitService.
|
|
4300
|
+
return GitService.fetchStatusLite({ background: true });
|
|
3897
4301
|
});
|
|
3898
4302
|
})["catch"](function (err) {
|
|
3899
4303
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3968,7 +4372,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3968
4372
|
setSelectedPaths(new Set([renamedPath]));
|
|
3969
4373
|
EditorStore.setStatus('Renamed to: ' + renamedPath, 'success');
|
|
3970
4374
|
return refreshProjectTree().then(function () {
|
|
3971
|
-
GitService.
|
|
4375
|
+
GitService.fetchStatusLite({ background: true });
|
|
3972
4376
|
});
|
|
3973
4377
|
})["catch"](function (err) {
|
|
3974
4378
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -4029,7 +4433,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4029
4433
|
EditorStore.setStatus('Delete failed: ' + message, 'error');
|
|
4030
4434
|
}
|
|
4031
4435
|
return refreshProjectTree().then(function () {
|
|
4032
|
-
GitService.
|
|
4436
|
+
GitService.fetchStatusLite({ background: true });
|
|
4033
4437
|
});
|
|
4034
4438
|
})["finally"](function () {
|
|
4035
4439
|
setLoading(function (prev) {
|
|
@@ -4052,6 +4456,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4052
4456
|
tabs: tabs,
|
|
4053
4457
|
activeId: activeId,
|
|
4054
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,
|
|
4055
4462
|
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
4056
4463
|
onSelect: function (id) {
|
|
4057
4464
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
@@ -4253,7 +4660,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4253
4660
|
"button",
|
|
4254
4661
|
{ className: "statusbar-btn", onClick: function () {
|
|
4255
4662
|
return activeTab && handleSave(focusedPane.id, activeTab);
|
|
4256
|
-
}, 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)" },
|
|
4257
4665
|
!loading.save && React.createElement("i", { className: "fas fa-save" }),
|
|
4258
4666
|
!toolbarIconOnly && !loading.save && " Save",
|
|
4259
4667
|
!loading.save && activeTab && activeTab.dirty ? " ●" : ""
|
|
@@ -4264,7 +4672,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4264
4672
|
return p.tabs;
|
|
4265
4673
|
}).some(function (t) {
|
|
4266
4674
|
return t.dirty;
|
|
4267
|
-
}), 'aria-busy': !!loading.saveAll },
|
|
4675
|
+
}), 'aria-busy': !!loading.saveAll, title: "Save every file with unsaved changes" },
|
|
4268
4676
|
!loading.saveAll && React.createElement(
|
|
4269
4677
|
"i",
|
|
4270
4678
|
{ className: "fas fa-save", style: { position: 'relative' } },
|
|
@@ -4308,7 +4716,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4308
4716
|
React.createElement("div", { className: "statusbar-sep" }),
|
|
4309
4717
|
React.createElement(
|
|
4310
4718
|
"button",
|
|
4311
|
-
{ 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)" },
|
|
4312
4721
|
React.createElement("i", { className: "fas fa-code-branch" }),
|
|
4313
4722
|
!toolbarIconOnly && " Git"
|
|
4314
4723
|
)
|
|
@@ -4523,7 +4932,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4523
4932
|
React.createElement(
|
|
4524
4933
|
"div",
|
|
4525
4934
|
{ className: "ide-body", id: "ide-body-container" },
|
|
4526
|
-
/* Activity bar — always visible,
|
|
4935
|
+
/* Activity bar — always visible, 60px wide */
|
|
4527
4936
|
!zenMode && React.createElement(
|
|
4528
4937
|
"div",
|
|
4529
4938
|
{ className: "ide-activity-bar" },
|
|
@@ -4666,6 +5075,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4666
5075
|
{
|
|
4667
5076
|
key: tab.id,
|
|
4668
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,
|
|
4669
5081
|
onClick: function () {
|
|
4670
5082
|
if (tab.path && !tab.path.startsWith('mbeditor://') && tab.path !== '__settings__') {
|
|
4671
5083
|
handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
|
|
@@ -4698,7 +5110,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4698
5110
|
"div",
|
|
4699
5111
|
{ className: "tab-close", onClick: function (e) {
|
|
4700
5112
|
e.stopPropagation();requestCloseTab(pane.id, tab.id);
|
|
4701
|
-
}, 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)" : "") },
|
|
4702
5117
|
React.createElement("i", { className: "fas fa-times" })
|
|
4703
5118
|
)
|
|
4704
5119
|
)
|
|
@@ -4758,6 +5173,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4758
5173
|
},
|
|
4759
5174
|
disabled: !!loading.createDir
|
|
4760
5175
|
}),
|
|
5176
|
+
React.createElement(SidebarActionButton, {
|
|
5177
|
+
title: "Upload files",
|
|
5178
|
+
iconClass: 'fas fa-upload',
|
|
5179
|
+
onClick: function () {
|
|
5180
|
+
return openImportDialog(selectedTreeNode);
|
|
5181
|
+
}
|
|
5182
|
+
}),
|
|
4761
5183
|
React.createElement(SidebarActionButton, {
|
|
4762
5184
|
title: "Rename selected",
|
|
4763
5185
|
iconClass: 'fas fa-pen',
|
|
@@ -4780,7 +5202,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4780
5202
|
)
|
|
4781
5203
|
},
|
|
4782
5204
|
React.createElement(FileTree, {
|
|
4783
|
-
items:
|
|
5205
|
+
items: fileTreeItems,
|
|
4784
5206
|
onSelect: handleSoftOpenFile,
|
|
4785
5207
|
activePath: editorPrefs.autoRevealInExplorer !== false ? (activeTab && activeTab.path) : null,
|
|
4786
5208
|
selectedPaths: selectedPaths,
|
|
@@ -4915,6 +5337,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4915
5337
|
var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
|
|
4916
5338
|
var hasAny = loadedCount > 0;
|
|
4917
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
|
+
|
|
4918
5348
|
return React.createElement(
|
|
4919
5349
|
React.Fragment,
|
|
4920
5350
|
null,
|
|
@@ -4932,30 +5362,38 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4932
5362
|
"div",
|
|
4933
5363
|
{
|
|
4934
5364
|
className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
|
|
4935
|
-
ref:
|
|
5365
|
+
ref: attachSearchResults,
|
|
4936
5366
|
onScroll: handleSearchResultsScroll
|
|
4937
5367
|
},
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
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" }),
|
|
4950
5385
|
React.createElement(
|
|
4951
|
-
"div", { className: "search-result-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
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
|
+
),
|
|
4959
5397
|
searchHasMore && React.createElement(
|
|
4960
5398
|
"div", { className: "search-loading-more" },
|
|
4961
5399
|
React.createElement("i", { className: "fas fa-spinner fa-spin" }),
|
|
@@ -5549,16 +5987,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5549
5987
|
React.createElement('option', { value: 'allDocuments' }, 'All open files')
|
|
5550
5988
|
)
|
|
5551
5989
|
),
|
|
5552
|
-
React.createElement(
|
|
5553
|
-
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Auto-format pasted code using the language formatter' },
|
|
5554
|
-
React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
|
|
5555
|
-
React.createElement('input', {
|
|
5556
|
-
type: 'checkbox',
|
|
5557
|
-
className: 'ide-settings-checkbox',
|
|
5558
|
-
checked: editorPrefs.formatOnPaste !== false,
|
|
5559
|
-
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
|
|
5560
|
-
})
|
|
5561
|
-
),
|
|
5562
5990
|
React.createElement(
|
|
5563
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 {})' },
|
|
5564
5992
|
React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
|
|
@@ -5743,6 +6171,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5743
6171
|
})
|
|
5744
6172
|
),
|
|
5745
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
|
+
|
|
5746
6185
|
/* ── RuboCop ─────────────────────────────────── */
|
|
5747
6186
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
|
|
5748
6187
|
React.createElement(
|
|
@@ -5774,6 +6213,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5774
6213
|
{
|
|
5775
6214
|
className: 'ide-settings-reset-btn',
|
|
5776
6215
|
type: 'button',
|
|
6216
|
+
title: 'Restore every editor preference on this page to its default',
|
|
5777
6217
|
onClick: function() { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); }
|
|
5778
6218
|
},
|
|
5779
6219
|
React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
|
|
@@ -5995,6 +6435,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5995
6435
|
onOpenFile: function (path, line, col) {
|
|
5996
6436
|
handleSelectFile(path, path.split('/').pop(), line, col);
|
|
5997
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
|
+
}
|
|
5998
6452
|
})
|
|
5999
6453
|
),
|
|
6000
6454
|
|
|
@@ -6061,6 +6515,19 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6061
6515
|
}),
|
|
6062
6516
|
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
|
|
6063
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
|
+
),
|
|
6064
6531
|
// ruby-lsp indicator. Hidden entirely when ruby-lsp was never available
|
|
6065
6532
|
// and nothing has gone wrong — a permanent "off" badge in a project with
|
|
6066
6533
|
// no Ruby is noise. A healthy server gets a quiet icon; a degraded one
|
|
@@ -6400,6 +6867,23 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6400
6867
|
" Delete"
|
|
6401
6868
|
),
|
|
6402
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" }),
|
|
6403
6887
|
React.createElement(
|
|
6404
6888
|
"div",
|
|
6405
6889
|
{ className: "context-menu-item", onClick: function () {
|
|
@@ -6475,6 +6959,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6475
6959
|
)
|
|
6476
6960
|
),
|
|
6477
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
|
+
|
|
6478
6970
|
/* ── Import conflict modal ─────────────────────────────────────────── */
|
|
6479
6971
|
importConflict && React.createElement(ImportConflictModal, {
|
|
6480
6972
|
conflicts: importConflict.result.conflicts,
|