mbeditor 0.12.9 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +216 -1
- data/app/assets/javascripts/mbeditor/application.js +6 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +52 -1
- 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 +104 -103
- 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 +224 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +704 -270
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +193 -9
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +105 -77
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +434 -112
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +112 -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 +50 -4
- data/app/assets/javascripts/mbeditor/tab_manager.js +66 -10
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +389 -103
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/collaboration_channel.rb +8 -2
- 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 +155 -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 +49 -3
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -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 +18 -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 +98 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +73 -66
- data/app/services/mbeditor/search_replace_service.rb +42 -7
- data/app/services/mbeditor/test_runner_service.rb +45 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +4 -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 +1 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +8 -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
|
|
|
@@ -201,6 +216,23 @@ var SectionActionGroup = function SectionActionGroup(_ref2) {
|
|
|
201
216
|
);
|
|
202
217
|
};
|
|
203
218
|
|
|
219
|
+
// Split a search hit into [before, match, after] so the match can be tinted
|
|
220
|
+
// and pinned on screen. `col`/`end_col` are 1-based against the RAW line while
|
|
221
|
+
// the row renders the stripped `text`, so `lead` — the characters strip took
|
|
222
|
+
// off the front — is what maps one onto the other. Returns null for the tiers
|
|
223
|
+
// and queries that produce no columns; the row then renders as plain text.
|
|
224
|
+
function searchMatchParts(res) {
|
|
225
|
+
var text = res.text == null ? "" : String(res.text);
|
|
226
|
+
if (!res.col || !res.end_col) return null;
|
|
227
|
+
var start = res.col - 1 - (res.lead || 0);
|
|
228
|
+
var end = Math.min(res.end_col - 1 - (res.lead || 0), text.length);
|
|
229
|
+
if (!(start >= 0 && end > start && start < text.length)) return null;
|
|
230
|
+
// U+200E: a strong LTR character, so the left-ellipsis trick in
|
|
231
|
+
// .search-result-pre (direction: rtl) can never reorder a segment that
|
|
232
|
+
// happens to be all punctuation.
|
|
233
|
+
return ["" + text.slice(0, start), text.slice(start, end), text.slice(end)];
|
|
234
|
+
}
|
|
235
|
+
|
|
204
236
|
function FileReloadBanner(_ref) {
|
|
205
237
|
var pendingReloads = _ref.pendingReloads;
|
|
206
238
|
var onSaveAndReload = _ref.onSaveAndReload;
|
|
@@ -347,6 +379,67 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
347
379
|
var searchWholeWordRef = useRef(false);
|
|
348
380
|
var searchResultsContainerRef = useRef(null);
|
|
349
381
|
|
|
382
|
+
// Search results are windowed: a project-wide query can return up to
|
|
383
|
+
// SearchReplaceService::MAX_RESULTS (10_000) rows, and every row is five
|
|
384
|
+
// elements, so rendering the list in full built ~50k nodes — enough to kill
|
|
385
|
+
// the tab outright, and 92 ms of render for a mere 3_000 rows. Rows are a
|
|
386
|
+
// fixed 22px, which is what makes the arithmetic here as simple as the file
|
|
387
|
+
// tree's. The results are a VS Code-style tree — a header row per file, its
|
|
388
|
+
// matches nested under it — but header and match rows are deliberately the
|
|
389
|
+
// *same* height (.search-result-file-row and .search-result-item both pin
|
|
390
|
+
// it), so the flattened row array still windows by plain multiplication.
|
|
391
|
+
// Give the two rows different heights and every offset here is wrong.
|
|
392
|
+
var SEARCH_ROW_HEIGHT = 22;
|
|
393
|
+
var SEARCH_ROW_BUFFER = 5;
|
|
394
|
+
|
|
395
|
+
// File paths whose match list is folded away. Keyed by path, so a file that
|
|
396
|
+
// scrolls out of the window keeps its state.
|
|
397
|
+
var _useStateSC = useState({});
|
|
398
|
+
var _useStateSC2 = _slicedToArray(_useStateSC, 2);
|
|
399
|
+
var searchCollapsedFiles = _useStateSC2[0];
|
|
400
|
+
var setSearchCollapsedFiles = _useStateSC2[1];
|
|
401
|
+
var toggleSearchFile = function toggleSearchFile(file) {
|
|
402
|
+
setSearchCollapsedFiles(function (prev) {
|
|
403
|
+
var next = Object.assign({}, prev);
|
|
404
|
+
if (next[file]) delete next[file]; else next[file] = true;
|
|
405
|
+
return next;
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
var _useStateSV = useState({ scrollTop: 0, height: 0 });
|
|
409
|
+
var _useStateSV2 = _slicedToArray(_useStateSV, 2);
|
|
410
|
+
var searchViewport = _useStateSV2[0];
|
|
411
|
+
var setSearchViewport = _useStateSV2[1];
|
|
412
|
+
|
|
413
|
+
// The container's height is only known once it is on screen, and a viewport
|
|
414
|
+
// of 0 would render a single row and never grow — nothing would scroll, so
|
|
415
|
+
// no scroll event would arrive to correct it.
|
|
416
|
+
//
|
|
417
|
+
// Measured from a callback ref rather than an effect: the results list
|
|
418
|
+
// mounts and unmounts as the sidebar tab changes and as a query goes from
|
|
419
|
+
// no-results to results, none of which an effect's dependency list sees.
|
|
420
|
+
// Same reason the model graph wires its pan/zoom this way.
|
|
421
|
+
var searchViewportObserverRef = useRef(null);
|
|
422
|
+
var measureSearchViewport = function measureSearchViewport(el) {
|
|
423
|
+
if (!el) return;
|
|
424
|
+
setSearchViewport(function (prev) {
|
|
425
|
+
if (prev.height === el.clientHeight && prev.scrollTop === el.scrollTop) return prev;
|
|
426
|
+
return { scrollTop: el.scrollTop, height: el.clientHeight };
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
var attachSearchResults = useRef(function (el) {
|
|
430
|
+
if (searchViewportObserverRef.current) {
|
|
431
|
+
searchViewportObserverRef.current.disconnect();
|
|
432
|
+
searchViewportObserverRef.current = null;
|
|
433
|
+
}
|
|
434
|
+
searchResultsContainerRef.current = el;
|
|
435
|
+
if (!el) return;
|
|
436
|
+
measureSearchViewport(el);
|
|
437
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
438
|
+
var obs = new ResizeObserver(function () { measureSearchViewport(el); });
|
|
439
|
+
obs.observe(el);
|
|
440
|
+
searchViewportObserverRef.current = obs;
|
|
441
|
+
}).current;
|
|
442
|
+
|
|
350
443
|
var _useStateRM = useState(false);
|
|
351
444
|
var _useStateRM2 = _slicedToArray(_useStateRM, 2);
|
|
352
445
|
var replaceMode = _useStateRM2[0];
|
|
@@ -408,6 +501,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
408
501
|
var importConflict = _useStateImportConflict2[0];
|
|
409
502
|
var setImportConflict = _useStateImportConflict2[1];
|
|
410
503
|
|
|
504
|
+
// { initialFolder } while the upload dialog is open, null otherwise.
|
|
505
|
+
var _useStateImportDialog = useState(null);
|
|
506
|
+
var _useStateImportDialog2 = _slicedToArray(_useStateImportDialog, 2);
|
|
507
|
+
var importDialog = _useStateImportDialog2[0];
|
|
508
|
+
var setImportDialog = _useStateImportDialog2[1];
|
|
509
|
+
|
|
411
510
|
var _useStateSchemaLoading = useState(null);
|
|
412
511
|
var _useStateSchemaLoading2 = _slicedToArray(_useStateSchemaLoading, 2);
|
|
413
512
|
var schemaLoadingLabel = _useStateSchemaLoading2[0];
|
|
@@ -544,13 +643,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
544
643
|
var modelGraphLoading = _useStateModelGraphLoading2[0];
|
|
545
644
|
var setModelGraphLoading = _useStateModelGraphLoading2[1];
|
|
546
645
|
|
|
547
|
-
// ruby-lsp status for the status-bar chip. 'off' means never available here,
|
|
548
|
-
// 'degraded' means we backed off after a failure, 'ok' means it's answering.
|
|
549
|
-
var _useStateLspHealth = useState({ status: 'off', reason: null });
|
|
550
|
-
var _useStateLspHealth2 = _slicedToArray(_useStateLspHealth, 2);
|
|
551
|
-
var lspHealth = _useStateLspHealth2[0];
|
|
552
|
-
var setLspHealth = _useStateLspHealth2[1];
|
|
553
|
-
|
|
554
646
|
var _useState18g = useState(320);
|
|
555
647
|
var _useState18g2 = _slicedToArray(_useState18g, 2);
|
|
556
648
|
var gitPanelWidth = _useState18g2[0];
|
|
@@ -606,11 +698,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
606
698
|
var rubocopConfigPath = _useState18rc2[0];
|
|
607
699
|
var setRubocopConfigPath = _useState18rc2[1];
|
|
608
700
|
|
|
609
|
-
var _useState18t = useState(false);
|
|
610
|
-
var _useState18t2 = _slicedToArray(_useState18t, 2);
|
|
611
|
-
var testAvailable = _useState18t2[0];
|
|
612
|
-
var setTestAvailable = _useState18t2[1];
|
|
613
|
-
|
|
614
701
|
var _useState18u = useState(null);
|
|
615
702
|
var _useState18u2 = _slicedToArray(_useState18u, 2);
|
|
616
703
|
var testResult = _useState18u2[0];
|
|
@@ -757,6 +844,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
757
844
|
if (typeof content === 'string') {
|
|
758
845
|
lastDiskContentRef.current[path] = content.replace(/\r\n/g, '\n');
|
|
759
846
|
}
|
|
847
|
+
// The file on disk just moved, so any line-diff answer we are holding for
|
|
848
|
+
// it predates the write and must not be reused for the tint.
|
|
849
|
+
if (typeof GitService !== 'undefined' && GitService.invalidateLineDiff) {
|
|
850
|
+
GitService.invalidateLineDiff(path);
|
|
851
|
+
}
|
|
760
852
|
}
|
|
761
853
|
|
|
762
854
|
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
@@ -821,6 +913,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
821
913
|
});
|
|
822
914
|
};
|
|
823
915
|
|
|
916
|
+
// filterDotFiles rebuilds the whole tree, so calling it inline in the render
|
|
917
|
+
// handed FileTreeMemo a fresh array every time and its `prev.items ===
|
|
918
|
+
// next.items` check never held — the memo was there but never fired.
|
|
919
|
+
var fileTreeItems = useMemo(function () {
|
|
920
|
+
return editorPrefs.showDotFiles ? treeData : filterDotFiles(treeData || []);
|
|
921
|
+
}, [treeData, editorPrefs.showDotFiles]);
|
|
922
|
+
|
|
824
923
|
var normalizeRelativePath = function normalizeRelativePath(input) {
|
|
825
924
|
return (input || "").replace(/\\/g, "/").trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/+/g, "/");
|
|
826
925
|
};
|
|
@@ -863,10 +962,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
863
962
|
// cached page was served for the same query. The per-path delta refresh on
|
|
864
963
|
// the files_changed push can't cover it either: it re-scans named files,
|
|
865
964
|
// and a file that just appeared or vanished isn't in the previous result set.
|
|
965
|
+
// True when the server will push a files_changed broadcast for a write we are
|
|
966
|
+
// about to make, so the coalesced handler will refresh the tree and git for
|
|
967
|
+
// us. Every mutation used to refresh both itself as well, which doubled or
|
|
968
|
+
// tripled the most expensive requests the editor makes. Without a socket
|
|
969
|
+
// nothing arrives, so callers keep their own refresh for that case.
|
|
970
|
+
var _socketWillBroadcast = function _socketWillBroadcast() {
|
|
971
|
+
return typeof WebSocketService !== 'undefined' &&
|
|
972
|
+
typeof WebSocketService.isCableAvailable === 'function' &&
|
|
973
|
+
WebSocketService.isCableAvailable();
|
|
974
|
+
};
|
|
975
|
+
|
|
866
976
|
var refreshProjectTree = function refreshProjectTree() {
|
|
867
977
|
return FileService.getTree().then(function (data) {
|
|
868
|
-
|
|
869
|
-
|
|
978
|
+
// _treeUpdater keeps the previous array (and skips the index rebuild)
|
|
979
|
+
// when the tree is unchanged; going round it re-rendered the whole app.
|
|
980
|
+
setTreeData(_treeUpdater(data || []));
|
|
870
981
|
SearchService.invalidate();
|
|
871
982
|
if (searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
872
983
|
_debouncedSearch(searchQueryRef.current);
|
|
@@ -930,73 +1041,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
930
1041
|
loadModelGraph(false);
|
|
931
1042
|
}, [activeSidebarTab, sidebarCollapsed]);
|
|
932
1043
|
|
|
933
|
-
|
|
934
|
-
if (!window.MBEDITOR_RUBY_LSP_AVAILABLE) {
|
|
935
|
-
return { status: 'off', reason: window.MBEDITOR_RUBY_LSP_REASON || null };
|
|
936
|
-
}
|
|
937
|
-
if (window.MbeditorEditorPlugins && MbeditorEditorPlugins.lspBackedOff()) {
|
|
938
|
-
return { status: 'degraded', reason: window.MBEDITOR_RUBY_LSP_REASON || null };
|
|
939
|
-
}
|
|
940
|
-
return { status: 'ok', reason: null };
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
// The backoff expires on a wall-clock deadline rather than a timer, so the
|
|
944
|
-
// chip also re-reads on a slow interval — otherwise it would sit on
|
|
945
|
-
// 'degraded' until the next failure or restart click.
|
|
1044
|
+
// The one writer for the markers map. Two things it must not do:
|
|
946
1045
|
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
setLspHealth(function (prev) {
|
|
955
|
-
var next = readLspHealth();
|
|
956
|
-
if (prev && prev.status === next.status && prev.reason === next.reason) return prev;
|
|
957
|
-
return next;
|
|
958
|
-
});
|
|
959
|
-
};
|
|
960
|
-
sync();
|
|
961
|
-
window.addEventListener('mbeditor:lsp-health', sync);
|
|
962
|
-
var tick = setInterval(sync, 10000);
|
|
963
|
-
return function () {
|
|
964
|
-
window.removeEventListener('mbeditor:lsp-health', sync);
|
|
965
|
-
clearInterval(tick);
|
|
966
|
-
};
|
|
967
|
-
}, []);
|
|
968
|
-
|
|
969
|
-
var restartRubyLsp = function restartRubyLsp() {
|
|
970
|
-
if (!FileService.rubyLspRequest) return;
|
|
971
|
-
EditorStore.setStatus('Restarting ruby-lsp…', 'info');
|
|
972
|
-
FileService.rubyLspRequest('restart', '', '', 1, 1).then(function (data) {
|
|
973
|
-
var ok = data && data.available && data.state !== 'failed';
|
|
974
|
-
if (ok) {
|
|
975
|
-
window.MBEDITOR_RUBY_LSP_AVAILABLE = true;
|
|
976
|
-
window.MBEDITOR_RUBY_LSP_DISABLED_UNTIL = 0;
|
|
977
|
-
window.MBEDITOR_RUBY_LSP_REASON = null;
|
|
978
|
-
} else {
|
|
979
|
-
window.MBEDITOR_RUBY_LSP_REASON =
|
|
980
|
-
(data && (data.reason || data.error)) || 'ruby-lsp did not come back';
|
|
981
|
-
}
|
|
982
|
-
EditorStore.setStatus(ok ? 'ruby-lsp restarted' : 'ruby-lsp unavailable', ok ? 'success' : 'warning');
|
|
983
|
-
setLspHealth(readLspHealth());
|
|
984
|
-
})["catch"](function (err) {
|
|
985
|
-
noteLspFailure(err);
|
|
986
|
-
EditorStore.setStatus('Could not restart ruby-lsp', 'error');
|
|
987
|
-
setLspHealth(readLspHealth());
|
|
988
|
-
});
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
var applyMarkersForTab = function applyMarkersForTab(paneId, tabId, nextMarkers) {
|
|
992
|
-
var currentPane = EditorStore.getState().panes.find(function (p) {
|
|
993
|
-
return p.id === paneId;
|
|
994
|
-
});
|
|
995
|
-
var current = currentPane ? currentPane.tabs.find(function (t) {
|
|
996
|
-
return t.id === tabId;
|
|
997
|
-
}) : null;
|
|
998
|
-
if (current) current.markers = nextMarkers;
|
|
1046
|
+
// * write a fresh map when nothing changed — the auto-lint fires per
|
|
1047
|
+
// keystroke on JS files and re-clearing an already-empty marker list
|
|
1048
|
+
// would re-render the whole app every time;
|
|
1049
|
+
// * mutate the tab object in the store — EditorStore's contract is that
|
|
1050
|
+
// every nested value is replaced, never edited in place, or
|
|
1051
|
+
// subscribeToSlice cannot see the change. TabBar reads this map instead.
|
|
1052
|
+
var applyMarkersForTab = function applyMarkersForTab(tabId, nextMarkers) {
|
|
999
1053
|
setMarkers(function (prev) {
|
|
1054
|
+
var cur = prev[tabId];
|
|
1055
|
+
if (cur && cur.length === 0 && nextMarkers.length === 0) return prev;
|
|
1000
1056
|
return _extends({}, prev, _defineProperty({}, tabId, nextMarkers));
|
|
1001
1057
|
});
|
|
1002
1058
|
};
|
|
@@ -1006,6 +1062,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1006
1062
|
|
|
1007
1063
|
if (!tab || (!isRubyPath(tab.path) && !tab.path.endsWith('.haml'))) return Promise.resolve(null);
|
|
1008
1064
|
|
|
1065
|
+
// An empty buffer has no offenses, and answering that here rather than over
|
|
1066
|
+
// the wire skips the most expensive request the Ruby path makes: a
|
|
1067
|
+
// whole-document RuboCop run, budgeted at 10s server-side. It matters
|
|
1068
|
+
// because every newly created .rb file starts empty and is opened
|
|
1069
|
+
// immediately, so the lint fired on a document with nothing in it.
|
|
1070
|
+
// Resolving with an empty marker set (rather than null) keeps the
|
|
1071
|
+
// marker-clearing below intact, so deleting a file's contents still clears
|
|
1072
|
+
// its squiggles.
|
|
1073
|
+
if (!String(tab.content || '').trim()) {
|
|
1074
|
+
applyMarkersForTab(tab.id, []);
|
|
1075
|
+
if (options.showStatus) EditorStore.setStatus('No RuboCop offenses!', 'success');
|
|
1076
|
+
return Promise.resolve({ markers: [] });
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1009
1079
|
if (options.showLoading) {
|
|
1010
1080
|
setLoading(function (prev) {
|
|
1011
1081
|
return _extends({}, prev, { lint: true });
|
|
@@ -1037,7 +1107,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1037
1107
|
|
|
1038
1108
|
return lintRequest.then(function (res) {
|
|
1039
1109
|
var nextMarkers = res.markers || [];
|
|
1040
|
-
applyMarkersForTab(
|
|
1110
|
+
applyMarkersForTab(tab.id, nextMarkers);
|
|
1041
1111
|
|
|
1042
1112
|
if (options.showStatus) {
|
|
1043
1113
|
var count = res.summary && res.summary.offense_count || 0;
|
|
@@ -1075,16 +1145,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1075
1145
|
if (parserName && window.prettier && window.prettierPlugins) {
|
|
1076
1146
|
var prefs = EditorStore.getState().editorPrefs || DEFAULT_EDITOR_PREFS;
|
|
1077
1147
|
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
|
-
});
|
|
1148
|
+
applyMarkersForTab(tab.id, []);
|
|
1088
1149
|
})["catch"](function (err) {
|
|
1089
1150
|
var newMarkers = [];
|
|
1090
1151
|
if (err && err.loc) {
|
|
@@ -1101,16 +1162,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1101
1162
|
endCol: endLoc ? endLoc.column : loc.column + 1
|
|
1102
1163
|
});
|
|
1103
1164
|
}
|
|
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
|
-
});
|
|
1165
|
+
applyMarkersForTab(tab.id, newMarkers);
|
|
1114
1166
|
});
|
|
1115
1167
|
}
|
|
1116
1168
|
}, 600)).current;
|
|
@@ -1168,8 +1220,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1168
1220
|
if (workspace && typeof workspace.redmineEnabled === 'boolean') {
|
|
1169
1221
|
setRedmineEnabled(workspace.redmineEnabled);
|
|
1170
1222
|
}
|
|
1171
|
-
if (workspace &&
|
|
1172
|
-
|
|
1223
|
+
if (workspace && workspace.testTimeout) {
|
|
1224
|
+
FileService.setTestTimeout(workspace.testTimeout);
|
|
1173
1225
|
}
|
|
1174
1226
|
if (workspace && typeof workspace.actionCableEnabled === 'boolean') {
|
|
1175
1227
|
WebSocketService.connect(workspace.actionCableEnabled);
|
|
@@ -1683,8 +1735,32 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1683
1735
|
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1684
1736
|
// and git status immediately (same work as the 10s poll below does).
|
|
1685
1737
|
useEffect(function () {
|
|
1686
|
-
|
|
1687
|
-
|
|
1738
|
+
// One broadcast per written file, so a bulk write (Save All, Format All,
|
|
1739
|
+
// a drag-and-drop import, a rename) used to cost one full tree walk and
|
|
1740
|
+
// one git status PER FILE, all in flight at once. On a real host repo
|
|
1741
|
+
// those are the two most expensive requests the editor makes, and with
|
|
1742
|
+
// the browser's six sockets per host the saves themselves ended up queued
|
|
1743
|
+
// behind their own fallout — past ~30 files the POSTs hit the 30 s axios
|
|
1744
|
+
// timeout and Save All reported failure for writes that had succeeded.
|
|
1745
|
+
// The refresh is idempotent, so a burst only needs one of each.
|
|
1746
|
+
var timer = null;
|
|
1747
|
+
var pendingPaths = [];
|
|
1748
|
+
var pendingFullCheck = false;
|
|
1749
|
+
var pendingStructural = false;
|
|
1750
|
+
|
|
1751
|
+
function flush() {
|
|
1752
|
+
timer = null;
|
|
1753
|
+
// The named paths and "re-check everything" are two separate questions.
|
|
1754
|
+
// Collapsing them lost the paths whenever a path-less broadcast (a new
|
|
1755
|
+
// directory, say) landed in the same window as a save, and the saved
|
|
1756
|
+
// file's search rows then stayed stale until the next edit.
|
|
1757
|
+
var paths = pendingPaths;
|
|
1758
|
+
var fullCheck = pendingFullCheck;
|
|
1759
|
+
var structural = pendingStructural;
|
|
1760
|
+
pendingPaths = [];
|
|
1761
|
+
pendingFullCheck = false;
|
|
1762
|
+
pendingStructural = false;
|
|
1763
|
+
var tabsToCheck = (fullCheck || paths.length === 0) ? null : paths;
|
|
1688
1764
|
// The cheap /git_status probe, not the full /git_info fan-out. This
|
|
1689
1765
|
// fires on every save, and the fan-out is the most expensive request
|
|
1690
1766
|
// the editor makes — on a dev server with a handful of threads it
|
|
@@ -1693,17 +1769,75 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1693
1769
|
// branch and file list immediately and escalates to the fan-out on its
|
|
1694
1770
|
// own when the branch actually changed.
|
|
1695
1771
|
GitService.fetchStatusLite({ background: true })["catch"](function () {});
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1772
|
+
|
|
1773
|
+
// A save changes a file's contents, not the shape of the workspace, so
|
|
1774
|
+
// there is nothing in the tree for it to invalidate: the names, the
|
|
1775
|
+
// nesting and the quick-open index are all exactly as they were. Walking
|
|
1776
|
+
// the whole workspace to learn that — and then rebuilding the MiniSearch
|
|
1777
|
+
// index from the result, because the file's byte count moved and made
|
|
1778
|
+
// the payload compare unequal — was pure cost on the save path, and it
|
|
1779
|
+
// grew with the size of the checkout rather than with the edit.
|
|
1780
|
+
// Git badges come from git_status above, not from this payload.
|
|
1781
|
+
if (structural) {
|
|
1782
|
+
FileService.getTree().then(function (data) {
|
|
1783
|
+
setTreeData(_treeUpdater(data || []));
|
|
1784
|
+
checkOpenTabsForExternalChanges(tabsToCheck);
|
|
1785
|
+
})["catch"](function () {});
|
|
1786
|
+
} else {
|
|
1787
|
+
checkOpenTabsForExternalChanges(tabsToCheck);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (paths.length && searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
1791
|
+
paths.forEach(function (p) { _pendingSearchRefreshPaths.current.add(p); });
|
|
1702
1792
|
_flushSearchRefresh();
|
|
1703
1793
|
}
|
|
1704
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
function handleFilesChanged(payload) {
|
|
1797
|
+
if (document.hidden) return;
|
|
1798
|
+
// No paths named means "something changed, re-check everything" — it
|
|
1799
|
+
// must not be swallowed by a burst that did name paths.
|
|
1800
|
+
if (!payload || !payload.paths) pendingFullCheck = true;
|
|
1801
|
+
else pendingPaths = pendingPaths.concat(payload.paths);
|
|
1802
|
+
// Absent (an older server) counts as structural — the conservative read.
|
|
1803
|
+
if (!payload || payload.structural !== false) pendingStructural = true;
|
|
1804
|
+
if (timer) return;
|
|
1805
|
+
timer = setTimeout(flush, 200);
|
|
1806
|
+
}
|
|
1807
|
+
// Deferring the refresh means it can now come due after the page has begun
|
|
1808
|
+
// going away. Requests issued into a closing page are aborted mid-flight,
|
|
1809
|
+
// which wastes a tree walk and leaves the server holding a connection that
|
|
1810
|
+
// never completes.
|
|
1811
|
+
var cancelPending = function () {
|
|
1812
|
+
if (timer) clearTimeout(timer);
|
|
1813
|
+
timer = null;
|
|
1814
|
+
};
|
|
1815
|
+
window.addEventListener('pagehide', cancelPending);
|
|
1816
|
+
|
|
1705
1817
|
WebSocketService.onFilesChanged(handleFilesChanged);
|
|
1706
|
-
return function () {
|
|
1818
|
+
return function () {
|
|
1819
|
+
cancelPending();
|
|
1820
|
+
window.removeEventListener('pagehide', cancelPending);
|
|
1821
|
+
WebSocketService.offFilesChanged(handleFilesChanged);
|
|
1822
|
+
};
|
|
1823
|
+
}, []);
|
|
1824
|
+
|
|
1825
|
+
// A branch switch changes every tracked file at once, and no push announces
|
|
1826
|
+
// it — the broadcast only covers mbeditor's own writes. Re-read the tree and
|
|
1827
|
+
// every open tab, the same work a manual workspace refresh does. Clean tabs
|
|
1828
|
+
// take the new branch's content; dirty ones queue the usual reload prompt,
|
|
1829
|
+
// so unsaved work is never overwritten.
|
|
1830
|
+
useEffect(function () {
|
|
1831
|
+
function onBranchChanged() {
|
|
1832
|
+
SearchService.invalidate();
|
|
1833
|
+
FileService.getTree({ refresh: true }).then(function (data) {
|
|
1834
|
+
setTreeData(_treeUpdater(data || []));
|
|
1835
|
+
})["catch"](function () {})["finally"](function () {
|
|
1836
|
+
checkOpenTabsForExternalChanges();
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
window.addEventListener('mbeditor:branch-changed', onBranchChanged);
|
|
1840
|
+
return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
|
|
1707
1841
|
}, []);
|
|
1708
1842
|
|
|
1709
1843
|
// WebSocket push — when a peer saves a collaboratively-bound file, the CRDT has
|
|
@@ -1839,6 +1973,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1839
1973
|
});
|
|
1840
1974
|
})
|
|
1841
1975
|
});
|
|
1976
|
+
// The text just moved under the diagnostics. Only the mounted editor
|
|
1977
|
+
// re-lints, so without this a background tab kept reporting offenses
|
|
1978
|
+
// at line numbers the external write had shifted — and the Problems
|
|
1979
|
+
// panel and status-bar tallies reported them too. Dropping them says
|
|
1980
|
+
// "not known yet", which is true: the file re-lints when you open it.
|
|
1981
|
+
discardStaleMarkers(pt.tab.path);
|
|
1842
1982
|
} else {
|
|
1843
1983
|
// Re-verify the tab still exists before queuing
|
|
1844
1984
|
var currentState = EditorStore.getState();
|
|
@@ -1926,7 +2066,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1926
2066
|
if (timer) clearTimeout(timer);
|
|
1927
2067
|
timer = setTimeout(function () {
|
|
1928
2068
|
timer = null;
|
|
1929
|
-
|
|
2069
|
+
var next = window.ProblemsPanel.counts();
|
|
2070
|
+
// Two object literals are never Object.is-equal, so handing React a
|
|
2071
|
+
// fresh {errors, warnings} re-rendered the app on every marker change
|
|
2072
|
+
// — which for a JS file is every keystroke. Compare the fields.
|
|
2073
|
+
setProblemCounts(function (prev) {
|
|
2074
|
+
return (prev && prev.errors === next.errors && prev.warnings === next.warnings) ? prev : next;
|
|
2075
|
+
});
|
|
1930
2076
|
}, 250);
|
|
1931
2077
|
};
|
|
1932
2078
|
|
|
@@ -1939,8 +2085,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1939
2085
|
};
|
|
1940
2086
|
}, [monacoReady]);
|
|
1941
2087
|
|
|
1942
|
-
var handleSelectFile = function handleSelectFile(path, name, line, col) {
|
|
1943
|
-
TabManager.openTab(path, name, line, null, false, col);
|
|
2088
|
+
var handleSelectFile = function handleSelectFile(path, name, line, col, endCol) {
|
|
2089
|
+
TabManager.openTab(path, name, line, null, false, col, endCol);
|
|
1944
2090
|
handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
|
|
1945
2091
|
setQuickOpen(false);
|
|
1946
2092
|
};
|
|
@@ -1968,6 +2114,33 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1968
2114
|
handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
|
|
1969
2115
|
};
|
|
1970
2116
|
|
|
2117
|
+
// Both of these are keyed by something that dies with the tab — markers by
|
|
2118
|
+
// tab id, the external-change baseline by path — and nothing dropped them, so
|
|
2119
|
+
// a long session held a full copy of every file it had ever opened. Run after
|
|
2120
|
+
// a close (single or bulk) and reconcile against what is still open: a path
|
|
2121
|
+
// the other pane still shows keeps its baseline, which a per-tab delete
|
|
2122
|
+
// would have got wrong.
|
|
2123
|
+
var forgetClosedTabs = function forgetClosedTabs() {
|
|
2124
|
+
var openIds = {};
|
|
2125
|
+
var openPaths = {};
|
|
2126
|
+
EditorStore.getState().panes.forEach(function (p) {
|
|
2127
|
+
p.tabs.forEach(function (t) {
|
|
2128
|
+
openIds[t.id] = true;
|
|
2129
|
+
if (t.path) openPaths[t.path] = true;
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
Object.keys(lastDiskContentRef.current).forEach(function (path) {
|
|
2133
|
+
if (!openPaths[path]) delete lastDiskContentRef.current[path];
|
|
2134
|
+
});
|
|
2135
|
+
setMarkers(function (prev) {
|
|
2136
|
+
var stale = Object.keys(prev).filter(function (id) { return !openIds[id]; });
|
|
2137
|
+
if (stale.length === 0) return prev;
|
|
2138
|
+
var next = _extends({}, prev);
|
|
2139
|
+
stale.forEach(function (id) { delete next[id]; });
|
|
2140
|
+
return next;
|
|
2141
|
+
});
|
|
2142
|
+
};
|
|
2143
|
+
|
|
1971
2144
|
var requestCloseTab = function requestCloseTab(paneId, id) {
|
|
1972
2145
|
var pane = state.panes.find(function (p) {
|
|
1973
2146
|
return p.id === paneId;
|
|
@@ -1987,6 +2160,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1987
2160
|
});
|
|
1988
2161
|
})
|
|
1989
2162
|
});
|
|
2163
|
+
forgetClosedTabs();
|
|
1990
2164
|
}
|
|
1991
2165
|
};
|
|
1992
2166
|
|
|
@@ -2040,6 +2214,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2040
2214
|
});
|
|
2041
2215
|
})
|
|
2042
2216
|
});
|
|
2217
|
+
forgetClosedTabs();
|
|
2043
2218
|
})["catch"](function (err) {
|
|
2044
2219
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2045
2220
|
})["finally"](function () {
|
|
@@ -2059,6 +2234,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2059
2234
|
});
|
|
2060
2235
|
})
|
|
2061
2236
|
});
|
|
2237
|
+
forgetClosedTabs();
|
|
2062
2238
|
setClosingTabId(null);
|
|
2063
2239
|
setClosingPaneId(null);
|
|
2064
2240
|
}
|
|
@@ -2083,6 +2259,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2083
2259
|
if (!confirmBulkClose(allTabs, "all editors")) return;
|
|
2084
2260
|
|
|
2085
2261
|
TabManager.closeAllTabs();
|
|
2262
|
+
forgetClosedTabs();
|
|
2086
2263
|
setClosingTabId(null);
|
|
2087
2264
|
setClosingPaneId(null);
|
|
2088
2265
|
EditorStore.setStatus("Closed " + allTabs.length + " editor" + (allTabs.length === 1 ? "" : "s"), "info");
|
|
@@ -2096,6 +2273,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2096
2273
|
if (!confirmBulkClose(pane.tabs, "all editors in Group " + paneId)) return;
|
|
2097
2274
|
|
|
2098
2275
|
TabManager.closeAllTabsInPane(paneId);
|
|
2276
|
+
forgetClosedTabs();
|
|
2099
2277
|
setClosingTabId(null);
|
|
2100
2278
|
setClosingPaneId(null);
|
|
2101
2279
|
EditorStore.setStatus("Closed " + pane.tabs.length + " editor" + (pane.tabs.length === 1 ? "" : "s") + " in Group " + paneId, "info");
|
|
@@ -2108,6 +2286,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2108
2286
|
if (others.length === 0) return;
|
|
2109
2287
|
if (!confirmBulkClose(others, "other editors")) return;
|
|
2110
2288
|
TabManager.closeOtherTabsInPane(paneId, keepId);
|
|
2289
|
+
forgetClosedTabs();
|
|
2111
2290
|
EditorStore.setStatus("Closed " + others.length + " editor" + (others.length === 1 ? "" : "s"), "info");
|
|
2112
2291
|
};
|
|
2113
2292
|
|
|
@@ -2117,6 +2296,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2117
2296
|
var saved = pane.tabs.filter(function (t) { return !t.dirty; });
|
|
2118
2297
|
if (saved.length === 0) return;
|
|
2119
2298
|
TabManager.closeSavedTabsInPane(paneId);
|
|
2299
|
+
forgetClosedTabs();
|
|
2120
2300
|
EditorStore.setStatus("Closed " + saved.length + " saved editor" + (saved.length === 1 ? "" : "s"), "info");
|
|
2121
2301
|
};
|
|
2122
2302
|
|
|
@@ -2134,6 +2314,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2134
2314
|
GitService.fetchStatusLite({ background: true });
|
|
2135
2315
|
FileService.getTree().then(function (data) { setTreeData(_treeUpdater(data || [])); })["catch"](function () {});
|
|
2136
2316
|
TabManager.closeTab(paneId, tab.id);
|
|
2317
|
+
forgetClosedTabs();
|
|
2137
2318
|
if (!(opts && opts.close)) {
|
|
2138
2319
|
TabManager.openTab(newPath, newPath.split('/').pop(), null, paneId);
|
|
2139
2320
|
}
|
|
@@ -2296,7 +2477,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2296
2477
|
var RAILS_MAX_RESOURCES = 10;
|
|
2297
2478
|
|
|
2298
2479
|
// Map resource label → representative path (capped at RAILS_MAX_RESOURCES, focused pane first)
|
|
2299
|
-
|
|
2480
|
+
// Memoised: all three of these walk every tab in every pane and were rebuilt
|
|
2481
|
+
// on every render, including the ones a keystroke causes.
|
|
2482
|
+
// resourceLabelFromPath reads customPathsRef, hence customPaths in the deps.
|
|
2483
|
+
var railsResourceDeps = useMemo(function() {
|
|
2300
2484
|
var deps = {};
|
|
2301
2485
|
var panesOrdered = state.panes.slice().sort(function(a, b) {
|
|
2302
2486
|
return a.id === state.focusedPaneId ? -1 : b.id === state.focusedPaneId ? 1 : 0;
|
|
@@ -2313,10 +2497,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2313
2497
|
});
|
|
2314
2498
|
});
|
|
2315
2499
|
return deps;
|
|
2316
|
-
})
|
|
2500
|
+
}, [state.panes, state.focusedPaneId, customPaths]);
|
|
2317
2501
|
var railsResourceDepStr = Object.keys(railsResourceDeps).sort().join('|');
|
|
2318
2502
|
|
|
2319
|
-
var railsOverflow = (function() {
|
|
2503
|
+
var railsOverflow = useMemo(function() {
|
|
2320
2504
|
var all = {};
|
|
2321
2505
|
state.panes.forEach(function(p) {
|
|
2322
2506
|
p.tabs.forEach(function(t) {
|
|
@@ -2326,9 +2510,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2326
2510
|
});
|
|
2327
2511
|
});
|
|
2328
2512
|
return Math.max(0, Object.keys(all).length - Object.keys(railsResourceDeps).length);
|
|
2329
|
-
})
|
|
2513
|
+
}, [state.panes, railsResourceDeps, customPaths]);
|
|
2330
2514
|
|
|
2331
|
-
var dirtyPaths = (function() {
|
|
2515
|
+
var dirtyPaths = useMemo(function() {
|
|
2332
2516
|
var set = {};
|
|
2333
2517
|
state.panes.forEach(function(p) {
|
|
2334
2518
|
p.tabs.forEach(function(t) {
|
|
@@ -2336,7 +2520,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2336
2520
|
});
|
|
2337
2521
|
});
|
|
2338
2522
|
return set;
|
|
2339
|
-
})
|
|
2523
|
+
}, [state.panes]);
|
|
2340
2524
|
|
|
2341
2525
|
useEffect(function() {
|
|
2342
2526
|
if (activeSidebarTab !== 'rails') return;
|
|
@@ -2552,17 +2736,43 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2552
2736
|
// unchanged state is an identical string and React bails rather than
|
|
2553
2737
|
// re-rendering the app every tick.
|
|
2554
2738
|
useEffect(function () {
|
|
2739
|
+
// A reconnect is not a fault. Action Cable drops and re-establishes its
|
|
2740
|
+
// socket routinely, and a dev server busy with a burst of saves is enough
|
|
2741
|
+
// to cause it — the cable shares the same thread pool as every request the
|
|
2742
|
+
// editor makes. Reporting the first non-connected sample made the chip
|
|
2743
|
+
// announce "Pairing off" during exactly the moments the editor was already
|
|
2744
|
+
// struggling, then clear a few seconds later, which reads as a second
|
|
2745
|
+
// failure rather than as the self-healing reconnect it actually is.
|
|
2746
|
+
//
|
|
2747
|
+
// So a transient state has to be seen twice before it is shown, with the
|
|
2748
|
+
// confirming check brought forward so a real outage still surfaces in
|
|
2749
|
+
// seconds. Hard failures are exempt: missing libraries, no Action Cable, a
|
|
2750
|
+
// server that does not advertise it, or a *rejected* handshake never heal
|
|
2751
|
+
// on their own, so they report on the first sample as before.
|
|
2752
|
+
var TRANSIENT_PROBLEMS = { connected: true };
|
|
2753
|
+
var unconfirmed = null;
|
|
2754
|
+
var recheckTimer = null;
|
|
2755
|
+
|
|
2555
2756
|
function check() {
|
|
2556
2757
|
if (typeof CollaborationService === 'undefined' ||
|
|
2557
2758
|
typeof CollaborationService.diagnostics !== 'function') return;
|
|
2558
2759
|
var d = CollaborationService.diagnostics();
|
|
2559
2760
|
// "Nobody else is here" is not a fault; everything else is.
|
|
2560
2761
|
var problem = d.firstProblem && d.firstProblem.key !== 'peers' ? d.firstProblem.key : null;
|
|
2762
|
+
var transient = !!problem && !!TRANSIENT_PROBLEMS[problem] && d.cableStatus !== 'rejected';
|
|
2763
|
+
|
|
2764
|
+
if (transient && unconfirmed !== problem) {
|
|
2765
|
+
unconfirmed = problem;
|
|
2766
|
+
clearTimeout(recheckTimer);
|
|
2767
|
+
recheckTimer = setTimeout(check, 3000);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
unconfirmed = transient ? problem : null;
|
|
2561
2771
|
setCollabTrouble(function (prev) { return prev === problem ? prev : problem; });
|
|
2562
2772
|
}
|
|
2563
2773
|
check();
|
|
2564
2774
|
var id = setInterval(check, 10000);
|
|
2565
|
-
return function () { clearInterval(id); };
|
|
2775
|
+
return function () { clearInterval(id); clearTimeout(recheckTimer); };
|
|
2566
2776
|
}, []);
|
|
2567
2777
|
|
|
2568
2778
|
var collabPeerIds = Object.keys(collabRoster);
|
|
@@ -2689,7 +2899,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2689
2899
|
|
|
2690
2900
|
// Clear markers and skip auto-lint when RuboCop linting is disabled
|
|
2691
2901
|
if (isRubyPath(activeTab.path) && editorPrefs.rubocopLintEnabled === false) {
|
|
2692
|
-
|
|
2902
|
+
applyMarkersForTab(activeTab.id, []);
|
|
2693
2903
|
return;
|
|
2694
2904
|
}
|
|
2695
2905
|
|
|
@@ -2767,7 +2977,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2767
2977
|
return runPrettier(tab.content, editorPrefs, parserName)["catch"](function () { return null; });
|
|
2768
2978
|
};
|
|
2769
2979
|
|
|
2980
|
+
// Re-read a tab from the store after flushing TabManager's throttled content
|
|
2981
|
+
// write. `tab` here is whatever the render captured, so flushing alone is not
|
|
2982
|
+
// enough — the fresh text lands in the store, not in this object.
|
|
2983
|
+
var _freshTab = function _freshTab(paneId, tab) {
|
|
2984
|
+
TabManager.flushContent();
|
|
2985
|
+
var pane = EditorStore.getState().panes.find(function (p) { return p.id === paneId; });
|
|
2986
|
+
var live = pane && pane.tabs.find(function (t) { return t.id === tab.id; });
|
|
2987
|
+
return live || tab;
|
|
2988
|
+
};
|
|
2989
|
+
|
|
2770
2990
|
var handleSave = function handleSave(paneId, tab) {
|
|
2991
|
+
tab = _freshTab(paneId, tab);
|
|
2771
2992
|
if (editorPrefs.formatOnSave === true) {
|
|
2772
2993
|
EditorStore.setStatus("Formatting " + tab.name + "...", "info");
|
|
2773
2994
|
_formatContentForSave(tab).then(function (formatted) {
|
|
@@ -2844,21 +3065,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2844
3065
|
FileService.lintFile(tab.path, tab.content, 'javascript').then(function (res) {
|
|
2845
3066
|
var babelMarkers = (res && res.markers) || [];
|
|
2846
3067
|
if (babelMarkers.length > 0) {
|
|
2847
|
-
applyMarkersForTab(
|
|
3068
|
+
applyMarkersForTab(tab.id, babelMarkers);
|
|
2848
3069
|
EditorStore.setStatus('Saved — babel: ' + babelMarkers[0].message, 'warning');
|
|
2849
3070
|
} else {
|
|
2850
3071
|
// Clear any previous babel marker for this tab (keep others none —
|
|
2851
3072
|
// 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
|
-
}
|
|
3073
|
+
// applyMarkersForTab keeps the previous map when it is already empty.
|
|
3074
|
+
applyMarkersForTab(tab.id, []);
|
|
2857
3075
|
}
|
|
2858
3076
|
})["catch"](function () {});
|
|
2859
3077
|
}
|
|
2860
3078
|
|
|
2861
|
-
|
|
3079
|
+
// The server broadcasts files_changed for this very write, and the
|
|
3080
|
+
// coalesced handler refreshes git status there — so firing here too ran
|
|
3081
|
+
// the git work twice per save, and this copy is the one a burst cannot
|
|
3082
|
+
// coalesce. fetchStatusLite escalates to the full /git_info fan-out
|
|
3083
|
+
// whenever the working tree signature moved, which saving a different
|
|
3084
|
+
// file every time does by definition, so the duplicate was the expensive
|
|
3085
|
+
// one. Without a socket no broadcast arrives, so keep it for that case.
|
|
3086
|
+
if (!_socketWillBroadcast()) GitService.fetchStatusLite({ background: true });
|
|
2862
3087
|
})["catch"](function (err) {
|
|
2863
3088
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2864
3089
|
})["finally"](function () {
|
|
@@ -2932,7 +3157,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2932
3157
|
}
|
|
2933
3158
|
|
|
2934
3159
|
var handleSaveAll = function handleSaveAll() {
|
|
2935
|
-
|
|
3160
|
+
// Flush first, then read the panes back out of the store — the render's
|
|
3161
|
+
// `state` predates the flush. See TabManager's flush contract.
|
|
3162
|
+
TabManager.flushContent();
|
|
3163
|
+
var dirtyTabs = EditorStore.getState().panes.flatMap(function (p) {
|
|
2936
3164
|
return p.tabs;
|
|
2937
3165
|
}).filter(function (t) {
|
|
2938
3166
|
// Untitled scratch tabs need a save-as prompt each — Ctrl+S them
|
|
@@ -2946,32 +3174,53 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2946
3174
|
});
|
|
2947
3175
|
EditorStore.setStatus("Saving " + dirtyTabs.length + " files...", "info");
|
|
2948
3176
|
isSavingRef.current = true;
|
|
3177
|
+
// allSettled, not all: one slow or rejected write used to abandon the
|
|
3178
|
+
// bookkeeping for every other file, so tabs whose contents were already on
|
|
3179
|
+
// disk stayed dirty and the only thing on offer was to hit Save All again.
|
|
3180
|
+
// Each file is now settled on its own result.
|
|
2949
3181
|
var promises = dirtyTabs.map(function (tab) {
|
|
2950
3182
|
return FileService.saveFile(tab.path, tab.content);
|
|
2951
3183
|
});
|
|
2952
|
-
Promise.
|
|
2953
|
-
dirtyTabs.
|
|
3184
|
+
Promise.allSettled(promises).then(function (results) {
|
|
3185
|
+
var saved = dirtyTabs.filter(function (tab, i) { return results[i].status === "fulfilled"; });
|
|
3186
|
+
// Object.create(null): these keys are file paths, and a file called
|
|
3187
|
+
// "constructor" would otherwise test as present against Object.prototype
|
|
3188
|
+
// and have its tab marked clean without ever being written.
|
|
3189
|
+
var savedPaths = Object.create(null);
|
|
3190
|
+
saved.forEach(function (tab) {
|
|
3191
|
+
savedPaths[tab.path] = tab.content;
|
|
2954
3192
|
noteLocalSave(tab.path, tab.content);
|
|
2955
3193
|
});
|
|
2956
3194
|
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
2957
3195
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3196
|
+
// Compare content, not just the path: a tab edited again while the
|
|
3197
|
+
// save was in flight is dirty against what actually reached disk.
|
|
3198
|
+
if (!(t.path in savedPaths) || t.content !== savedPaths[t.path]) return t;
|
|
2958
3199
|
return _extends({}, t, { dirty: false, cleanContent: t.content });
|
|
2959
3200
|
})
|
|
2960
3201
|
});
|
|
2961
3202
|
});
|
|
2962
3203
|
EditorStore.setState({ panes: newPanes });
|
|
2963
3204
|
// Reset AVI clean baselines for all saved files so undo past save shows dirty correctly.
|
|
2964
|
-
|
|
3205
|
+
saved.forEach(function(tab) {
|
|
2965
3206
|
var _me = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
2966
3207
|
if (_me && _me.model && !_me.model.isDisposed()) {
|
|
2967
3208
|
_me.cleanVersionId = _me.model.getAlternativeVersionId();
|
|
2968
3209
|
}
|
|
2969
3210
|
});
|
|
2970
|
-
|
|
3211
|
+
if (saved.length === dirtyTabs.length) {
|
|
3212
|
+
EditorStore.setStatus("All files saved", "success");
|
|
3213
|
+
} else {
|
|
3214
|
+
EditorStore.setStatus("Saved " + saved.length + " of " + dirtyTabs.length +
|
|
3215
|
+
" files — " + (dirtyTabs.length - saved.length) + " failed", "error");
|
|
3216
|
+
}
|
|
2971
3217
|
SearchService.invalidate();
|
|
2972
3218
|
GitService.fetchStatusLite({ background: true });
|
|
2973
|
-
})["catch"](function (
|
|
2974
|
-
|
|
3219
|
+
})["catch"](function () {
|
|
3220
|
+
// allSettled never rejects, so this only fires if the bookkeeping above
|
|
3221
|
+
// throws. Without it that would be an unhandled rejection and the status
|
|
3222
|
+
// bar would sit on "Saving N files..." forever.
|
|
3223
|
+
EditorStore.setStatus("Save All finished with errors", "error");
|
|
2975
3224
|
})["finally"](function () {
|
|
2976
3225
|
isSavingRef.current = false;
|
|
2977
3226
|
return setLoading(function (prev) {
|
|
@@ -3029,7 +3278,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3029
3278
|
return _extends({}, prev, { refreshWorkspace: true });
|
|
3030
3279
|
});
|
|
3031
3280
|
GitService.fetchStatus()["catch"](function () {});
|
|
3032
|
-
FileService.getTree().then(function (data) {
|
|
3281
|
+
FileService.getTree({ refresh: true }).then(function (data) {
|
|
3033
3282
|
setTreeData(_treeUpdater(data || []));
|
|
3034
3283
|
checkOpenTabsForExternalChanges();
|
|
3035
3284
|
EditorStore.setStatus("Workspace refreshed", "success");
|
|
@@ -3095,19 +3344,53 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3095
3344
|
return runPrettier(tab.content, prefs, parserName);
|
|
3096
3345
|
};
|
|
3097
3346
|
|
|
3347
|
+
// Markers are per-model and only the mounted editor re-lints, so a document
|
|
3348
|
+
// formatted in the background kept the diagnostics of the text it no longer
|
|
3349
|
+
// holds — the Problems panel and the status-bar tallies went on reporting
|
|
3350
|
+
// offenses at line numbers that had moved, and Format All barely moved the
|
|
3351
|
+
// counts because only the visible tab was re-checked. Dropping them says
|
|
3352
|
+
// "not known yet", which is true: the file re-lints when you open it.
|
|
3353
|
+
var discardStaleMarkers = function discardStaleMarkers(path) {
|
|
3354
|
+
if (!path) return;
|
|
3355
|
+
|
|
3356
|
+
// The React map has to go too, not just Monaco's copy. It is what TabBar
|
|
3357
|
+
// counts, and EditorPanel re-applies it to the model the next time that tab
|
|
3358
|
+
// mounts — so clearing only Monaco left every background tab primed to put
|
|
3359
|
+
// its stale squiggles straight back on the next tab switch.
|
|
3360
|
+
EditorStore.getState().panes.forEach(function (p) {
|
|
3361
|
+
p.tabs.forEach(function (t) {
|
|
3362
|
+
if (t.path === path) applyMarkersForTab(t.id, []);
|
|
3363
|
+
});
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
if (!window.monaco || !window.monaco.editor) return;
|
|
3367
|
+
var entry = window.__mbeditorModels && window.__mbeditorModels[path];
|
|
3368
|
+
if (!entry || !entry.model || entry.model.isDisposed()) return;
|
|
3369
|
+
|
|
3370
|
+
var owners = {};
|
|
3371
|
+
window.monaco.editor.getModelMarkers({ resource: entry.model.uri }).forEach(function (m) {
|
|
3372
|
+
owners[m.owner] = true;
|
|
3373
|
+
});
|
|
3374
|
+
Object.keys(owners).forEach(function (owner) {
|
|
3375
|
+
window.monaco.editor.setModelMarkers(entry.model, owner, []);
|
|
3376
|
+
});
|
|
3377
|
+
};
|
|
3378
|
+
|
|
3098
3379
|
// Write formatted content back to a tab, dirty and unsaved — the user decides
|
|
3099
3380
|
// when to save. EditorPanel applies it through executeEdits, so undo works.
|
|
3100
3381
|
var applyFormattedContent = function applyFormattedContent(paneId, tabId, formatted) {
|
|
3382
|
+
var formattedPath = null;
|
|
3101
3383
|
EditorStore.setState({
|
|
3102
3384
|
panes: EditorStore.getState().panes.map(function (p) {
|
|
3103
3385
|
if (p.id !== paneId) return p;
|
|
3104
3386
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3387
|
+
if (t.id !== tabId) return t;
|
|
3388
|
+
formattedPath = t.path;
|
|
3389
|
+
return _extends({}, t, { content: formatted, dirty: true, externalContentVersion: (t.externalContentVersion || 0) + 1 });
|
|
3108
3390
|
}) });
|
|
3109
3391
|
})
|
|
3110
3392
|
});
|
|
3393
|
+
discardStaleMarkers(formattedPath);
|
|
3111
3394
|
};
|
|
3112
3395
|
|
|
3113
3396
|
var highlightFormatChanges = function highlightFormatChanges(before, after) {
|
|
@@ -3124,8 +3407,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3124
3407
|
var handleFormat = function handleFormat() {
|
|
3125
3408
|
if (!activeTab) return;
|
|
3126
3409
|
|
|
3127
|
-
var tab = activeTab;
|
|
3128
3410
|
var paneId = focusedPane.id;
|
|
3411
|
+
var tab = _freshTab(paneId, activeTab);
|
|
3129
3412
|
var originalContent = tab.content;
|
|
3130
3413
|
|
|
3131
3414
|
setLoading(function (prev) { return _extends({}, prev, { format: true }); });
|
|
@@ -3163,6 +3446,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3163
3446
|
// thrown, so one unparseable file cannot stop the rest. Virtual tabs (diffs,
|
|
3164
3447
|
// settings, the changelog) have no path on disk and are skipped.
|
|
3165
3448
|
var handleFormatAll = function handleFormatAll() {
|
|
3449
|
+
// See TabManager's flush contract — the panes are read straight after.
|
|
3450
|
+
TabManager.flushContent();
|
|
3166
3451
|
var targets = [];
|
|
3167
3452
|
EditorStore.getState().panes.forEach(function (p) {
|
|
3168
3453
|
p.tabs.forEach(function (t) {
|
|
@@ -3209,15 +3494,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3209
3494
|
|
|
3210
3495
|
var TEST_CACHE_PREFIX = 'mbeditor_test_result_';
|
|
3211
3496
|
|
|
3212
|
-
var loadCachedTestResult = function loadCachedTestResult(filePath) {
|
|
3213
|
-
try {
|
|
3214
|
-
var stored = localStorage.getItem(TEST_CACHE_PREFIX + filePath);
|
|
3215
|
-
return stored ? JSON.parse(stored) : null;
|
|
3216
|
-
} catch (e) {
|
|
3217
|
-
return null;
|
|
3218
|
-
}
|
|
3219
|
-
};
|
|
3220
|
-
|
|
3221
3497
|
var saveCachedTestResult = function saveCachedTestResult(filePath, result) {
|
|
3222
3498
|
try {
|
|
3223
3499
|
localStorage.setItem(TEST_CACHE_PREFIX + filePath, JSON.stringify(result));
|
|
@@ -3259,21 +3535,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3259
3535
|
});
|
|
3260
3536
|
};
|
|
3261
3537
|
|
|
3262
|
-
var handleRunTest = function handleRunTest() {
|
|
3263
|
-
if (!activeTab || !activeTab.path) return;
|
|
3264
|
-
if (testLoading) return;
|
|
3265
|
-
|
|
3266
|
-
var cached = loadCachedTestResult(activeTab.path);
|
|
3267
|
-
if (cached && !testPanelOpen) {
|
|
3268
|
-
setTestResult(cached);
|
|
3269
|
-
setTestPanelFile(cached.testFile || activeTab.path);
|
|
3270
|
-
setTestPanelOpen(true);
|
|
3271
|
-
return;
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
executeTestRun(activeTab.path);
|
|
3275
|
-
};
|
|
3276
|
-
|
|
3277
3538
|
var handleRerunTest = function handleRerunTest() {
|
|
3278
3539
|
if (!activeTab || !activeTab.path) return;
|
|
3279
3540
|
if (testLoading) return;
|
|
@@ -3314,6 +3575,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3314
3575
|
searchOffsetRef.current = 0;
|
|
3315
3576
|
searchLoadingMoreRef.current = false;
|
|
3316
3577
|
searchQueryRef.current = q;
|
|
3578
|
+
setSearchCollapsedFiles({});
|
|
3317
3579
|
EditorStore.setState({ searchResults: [], searchHasMore: false });
|
|
3318
3580
|
EditorStore.setStatus("Searching project...", "info");
|
|
3319
3581
|
SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function (res) {
|
|
@@ -3430,8 +3692,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3430
3692
|
// Update any open Monaco models whose content changed.
|
|
3431
3693
|
if (files.length && window.__mbeditorModels) {
|
|
3432
3694
|
files.forEach(function(relPath) {
|
|
3433
|
-
|
|
3434
|
-
|
|
3695
|
+
// The registry holds {model, aviBase, ...} entries, not models. This
|
|
3696
|
+
// used to call setValue/isDisposed straight on the entry, which threw
|
|
3697
|
+
// into the .catch below — so an open tab kept its pre-replace text and
|
|
3698
|
+
// could save it back over the replacement.
|
|
3699
|
+
var entry = window.__mbeditorModels[relPath];
|
|
3700
|
+
var model = entry && entry.model;
|
|
3701
|
+
if (!model || model.isDisposed()) return;
|
|
3435
3702
|
FileService.getFile(relPath).then(function(res) {
|
|
3436
3703
|
if (res && res.content != null && !model.isDisposed()) {
|
|
3437
3704
|
model.setValue(res.content);
|
|
@@ -3457,6 +3724,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3457
3724
|
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
3458
3725
|
loadMoreSearchResults();
|
|
3459
3726
|
}
|
|
3727
|
+
setSearchViewport(function (prev) {
|
|
3728
|
+
// Only the rows that moved in or out of the window matter, and those
|
|
3729
|
+
// change a row at a time. Comparing keeps a scroll gesture from
|
|
3730
|
+
// committing a render per pixel.
|
|
3731
|
+
if (prev.scrollTop === el.scrollTop && prev.height === el.clientHeight) return prev;
|
|
3732
|
+
return { scrollTop: el.scrollTop, height: el.clientHeight };
|
|
3733
|
+
});
|
|
3460
3734
|
};
|
|
3461
3735
|
|
|
3462
3736
|
var toggleGitPanel = function toggleGitPanel() {
|
|
@@ -3556,12 +3830,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3556
3830
|
});
|
|
3557
3831
|
};
|
|
3558
3832
|
|
|
3559
|
-
var finishImport = function finishImport(result) {
|
|
3560
|
-
var
|
|
3833
|
+
var finishImport = function finishImport(result, destFolder) {
|
|
3834
|
+
var written = result.imported || [];
|
|
3835
|
+
var imported = written.length;
|
|
3561
3836
|
var skipped = (result.conflicts || []).length;
|
|
3562
3837
|
var failed = (result.errors || []).length;
|
|
3563
3838
|
|
|
3564
|
-
|
|
3839
|
+
// Say where the files went. A bulk upload that reports only a count looks
|
|
3840
|
+
// the same whether it landed where you meant it to or in the workspace
|
|
3841
|
+
// root, and the destination is the whole question a folder import raises.
|
|
3842
|
+
var where = destFolder ? ' to ' + destFolder : ' to the workspace root';
|
|
3843
|
+
var parts = [imported + ' file' + (imported === 1 ? '' : 's') + ' imported' + (imported > 0 ? where : '')];
|
|
3565
3844
|
if (skipped > 0) parts.push(skipped + ' skipped');
|
|
3566
3845
|
if (failed > 0) parts.push(failed + ' failed');
|
|
3567
3846
|
|
|
@@ -3569,7 +3848,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3569
3848
|
EditorStore.setStatus(parts.join(', ') + '.', level);
|
|
3570
3849
|
|
|
3571
3850
|
if (imported > 0) {
|
|
3572
|
-
|
|
3851
|
+
// Expand down to what was just written, so the tree actually shows it —
|
|
3852
|
+
// importing into a collapsed folder otherwise leaves the explorer looking
|
|
3853
|
+
// untouched. Deliberately does not *select* the folder: the tree
|
|
3854
|
+
// selection is what the toolbar's Upload button reads for its default
|
|
3855
|
+
// destination, and pinning it here would make every later upload default
|
|
3856
|
+
// to this import's folder.
|
|
3857
|
+
var landed = parentDir(written[0].path);
|
|
3858
|
+
var toExpand = {};
|
|
3859
|
+
var bits = landed ? landed.split('/') : [];
|
|
3860
|
+
for (var i = 1; i <= bits.length; i++) toExpand[bits.slice(0, i).join('/')] = true;
|
|
3861
|
+
|
|
3862
|
+
refreshProjectTree().then(function() {
|
|
3863
|
+
if (bits.length) setExpandedDirs(function (prev) { return Object.assign({}, prev, toExpand); });
|
|
3864
|
+
GitService.fetchStatus();
|
|
3865
|
+
});
|
|
3573
3866
|
}
|
|
3574
3867
|
};
|
|
3575
3868
|
|
|
@@ -3613,7 +3906,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3613
3906
|
if (result.conflicts && result.conflicts.length > 0) {
|
|
3614
3907
|
setImportConflict({ result: result, entries: entries, targetFolderPath: targetFolderPath });
|
|
3615
3908
|
} else {
|
|
3616
|
-
finishImport(result);
|
|
3909
|
+
finishImport(result, targetFolderPath);
|
|
3617
3910
|
}
|
|
3618
3911
|
})['catch'](function(err) {
|
|
3619
3912
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3626,14 +3919,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3626
3919
|
setImportConflict(null);
|
|
3627
3920
|
if (!pending) return;
|
|
3628
3921
|
|
|
3629
|
-
if (mode === 'skip') { finishImport(pending.result); return; }
|
|
3922
|
+
if (mode === 'skip') { finishImport(pending.result, pending.targetFolderPath); return; }
|
|
3630
3923
|
|
|
3631
3924
|
var retry = FileImport.conflictedEntries(
|
|
3632
3925
|
pending.entries,
|
|
3633
3926
|
pending.targetFolderPath,
|
|
3634
3927
|
pending.result.conflicts
|
|
3635
3928
|
);
|
|
3636
|
-
if (retry.length === 0) { finishImport(pending.result); return; }
|
|
3929
|
+
if (retry.length === 0) { finishImport(pending.result, pending.targetFolderPath); return; }
|
|
3637
3930
|
|
|
3638
3931
|
FileService.importFiles(FileImport.buildFormData(retry, pending.targetFolderPath, mode))
|
|
3639
3932
|
.then(function(second) {
|
|
@@ -3641,13 +3934,47 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3641
3934
|
imported: (pending.result.imported || []).concat(second.imported || []),
|
|
3642
3935
|
conflicts: [],
|
|
3643
3936
|
errors: (pending.result.errors || []).concat(second.errors || [])
|
|
3644
|
-
});
|
|
3937
|
+
}, pending.targetFolderPath);
|
|
3645
3938
|
})['catch'](function(err) {
|
|
3646
3939
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
3647
3940
|
EditorStore.setStatus('Import failed: ' + message, 'error');
|
|
3648
3941
|
});
|
|
3649
3942
|
};
|
|
3650
3943
|
|
|
3944
|
+
// Downloads go straight through /raw?download=1 — the browser's own save
|
|
3945
|
+
// flow, so nothing is buffered client-side and a 5 MB file costs no memory.
|
|
3946
|
+
// A synthetic anchor rather than location.href: navigating away from the
|
|
3947
|
+
// editor to an attachment response leaves the page in a half-unloaded state
|
|
3948
|
+
// in Safari.
|
|
3949
|
+
var handleDownloadFile = function handleDownloadFile(node) {
|
|
3950
|
+
if (!node || node.type !== 'file') return;
|
|
3951
|
+
var link = document.createElement('a');
|
|
3952
|
+
link.href = window.mbeditorBasePath() + '/raw?download=1&path=' + encodeURIComponent(node.path);
|
|
3953
|
+
link.download = node.name;
|
|
3954
|
+
link.rel = 'noopener';
|
|
3955
|
+
document.body.appendChild(link);
|
|
3956
|
+
link.click();
|
|
3957
|
+
document.body.removeChild(link);
|
|
3958
|
+
EditorStore.setStatus('Downloading ' + node.name + '...', 'info');
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
// A file node means "upload alongside this file", so the dialog opens on its
|
|
3962
|
+
// parent — right-clicking a file to upload next to it is the common gesture.
|
|
3963
|
+
var openImportDialog = function openImportDialog(node) {
|
|
3964
|
+
var folder = '';
|
|
3965
|
+
if (node && node.type === 'folder') {
|
|
3966
|
+
folder = node.path;
|
|
3967
|
+
} else if (node && node.path) {
|
|
3968
|
+
folder = node.path.split('/').slice(0, -1).join('/');
|
|
3969
|
+
}
|
|
3970
|
+
setImportDialog({ initialFolder: folder });
|
|
3971
|
+
};
|
|
3972
|
+
|
|
3973
|
+
var confirmImportDialog = function confirmImportDialog(entries, destFolder, meta) {
|
|
3974
|
+
setImportDialog(null);
|
|
3975
|
+
handleImportFiles(entries, destFolder, meta);
|
|
3976
|
+
};
|
|
3977
|
+
|
|
3651
3978
|
var openContextMenu = function openContextMenu(e, node) {
|
|
3652
3979
|
setContextMenu({ x: e.clientX, y: e.clientY, node: node });
|
|
3653
3980
|
handleNodeSelect(node);
|
|
@@ -3675,6 +4002,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3675
4002
|
if (action === 'delete') {
|
|
3676
4003
|
handleDeletePath(node);return;
|
|
3677
4004
|
}
|
|
4005
|
+
if (action === 'download' && node) {
|
|
4006
|
+
handleDownloadFile(node);return;
|
|
4007
|
+
}
|
|
4008
|
+
if (action === 'upload') {
|
|
4009
|
+
openImportDialog(node);return;
|
|
4010
|
+
}
|
|
3678
4011
|
if (action === 'copyPath' && node) {
|
|
3679
4012
|
if (navigator.clipboard) {
|
|
3680
4013
|
navigator.clipboard.writeText(node.path)["catch"](function () {});
|
|
@@ -3823,15 +4156,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3823
4156
|
activeTabId: activePane ? activePane.activeTabId : null
|
|
3824
4157
|
});
|
|
3825
4158
|
|
|
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
|
-
}
|
|
4159
|
+
if (removedTabIds.length) forgetClosedTabs();
|
|
3835
4160
|
};
|
|
3836
4161
|
|
|
3837
4162
|
// Reveal a folder path in the explorer: switch to explorer tab and expand
|
|
@@ -3890,8 +4215,27 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3890
4215
|
FileService.createFile(path, '').then(function (res) {
|
|
3891
4216
|
var createdPath = res && res.path || path;
|
|
3892
4217
|
var createdName = createdPath.split('/').pop();
|
|
4218
|
+
// We just wrote this file and know it is empty, so opening it does not
|
|
4219
|
+
// need to ask the server what is in it. noteLocalSave records the write
|
|
4220
|
+
// for the same reason a save does: the create's own broadcast comes
|
|
4221
|
+
// straight back as a files_changed naming this path, and without this
|
|
4222
|
+
// the external-change check read the file back off disk to ask whether
|
|
4223
|
+
// it had changed since we wrote it a moment earlier.
|
|
4224
|
+
if (FileService.seedPrefetch) FileService.seedPrefetch(createdPath, '');
|
|
4225
|
+
noteLocalSave(createdPath, '');
|
|
3893
4226
|
handleNodeSelect({ path: createdPath, name: createdName, type: 'file' });
|
|
3894
4227
|
EditorStore.setStatus('Created file: ' + createdName, 'success');
|
|
4228
|
+
// The optimistic node is already in the tree and the server's
|
|
4229
|
+
// structural broadcast refreshes it for real a moment later, so
|
|
4230
|
+
// walking the workspace here as well made one create cost three full
|
|
4231
|
+
// tree walks — and every write invalidates the server's tree cache, so
|
|
4232
|
+
// each one is a fresh recursive scan of the whole checkout. Same for
|
|
4233
|
+
// git status, which the same broadcast handler already refreshes.
|
|
4234
|
+
// Without a socket neither happens on its own, so keep both for that.
|
|
4235
|
+
if (_socketWillBroadcast()) {
|
|
4236
|
+
handleSelectFile(createdPath, createdName);
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
3895
4239
|
return refreshProjectTree().then(function () {
|
|
3896
4240
|
handleSelectFile(createdPath, createdName);
|
|
3897
4241
|
GitService.fetchStatusLite({ background: true });
|
|
@@ -3914,6 +4258,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3914
4258
|
var createdPath = res && res.path || path;
|
|
3915
4259
|
handleNodeSelect({ path: createdPath, name: createdPath.split('/').pop(), type: 'folder' });
|
|
3916
4260
|
EditorStore.setStatus('Created folder: ' + createdPath, 'success');
|
|
4261
|
+
// See handleCreateConfirm's file branch — the broadcast covers both.
|
|
4262
|
+
if (_socketWillBroadcast()) return;
|
|
3917
4263
|
return refreshProjectTree().then(function () {
|
|
3918
4264
|
return GitService.fetchStatusLite({ background: true });
|
|
3919
4265
|
});
|
|
@@ -4074,6 +4420,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4074
4420
|
tabs: tabs,
|
|
4075
4421
|
activeId: activeId,
|
|
4076
4422
|
paneId: paneId,
|
|
4423
|
+
// Diagnostics live in this map, not on the tab object — writing them onto
|
|
4424
|
+
// the tab would mutate a value inside EditorStore.
|
|
4425
|
+
markers: markers,
|
|
4077
4426
|
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
4078
4427
|
onSelect: function (id) {
|
|
4079
4428
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
@@ -4275,7 +4624,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4275
4624
|
"button",
|
|
4276
4625
|
{ className: "statusbar-btn", onClick: function () {
|
|
4277
4626
|
return activeTab && handleSave(focusedPane.id, activeTab);
|
|
4278
|
-
}, disabled: loading.save || !activeTab || !activeTab.dirty, 'aria-busy': !!loading.save
|
|
4627
|
+
}, disabled: loading.save || !activeTab || !activeTab.dirty, 'aria-busy': !!loading.save,
|
|
4628
|
+
title: "Save the active file (Ctrl+S)" },
|
|
4279
4629
|
!loading.save && React.createElement("i", { className: "fas fa-save" }),
|
|
4280
4630
|
!toolbarIconOnly && !loading.save && " Save",
|
|
4281
4631
|
!loading.save && activeTab && activeTab.dirty ? " ●" : ""
|
|
@@ -4286,7 +4636,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4286
4636
|
return p.tabs;
|
|
4287
4637
|
}).some(function (t) {
|
|
4288
4638
|
return t.dirty;
|
|
4289
|
-
}), 'aria-busy': !!loading.saveAll },
|
|
4639
|
+
}), 'aria-busy': !!loading.saveAll, title: "Save every file with unsaved changes" },
|
|
4290
4640
|
!loading.saveAll && React.createElement(
|
|
4291
4641
|
"i",
|
|
4292
4642
|
{ className: "fas fa-save", style: { position: 'relative' } },
|
|
@@ -4330,7 +4680,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4330
4680
|
React.createElement("div", { className: "statusbar-sep" }),
|
|
4331
4681
|
React.createElement(
|
|
4332
4682
|
"button",
|
|
4333
|
-
{ type: "button", className: "statusbar-btn", onClick: toggleGitPanel
|
|
4683
|
+
{ type: "button", className: "statusbar-btn", onClick: toggleGitPanel,
|
|
4684
|
+
title: (showGitPanel ? "Hide" : "Show") + " the git panel (Ctrl+Shift+G)" },
|
|
4334
4685
|
React.createElement("i", { className: "fas fa-code-branch" }),
|
|
4335
4686
|
!toolbarIconOnly && " Git"
|
|
4336
4687
|
)
|
|
@@ -4545,7 +4896,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4545
4896
|
React.createElement(
|
|
4546
4897
|
"div",
|
|
4547
4898
|
{ className: "ide-body", id: "ide-body-container" },
|
|
4548
|
-
/* Activity bar — always visible,
|
|
4899
|
+
/* Activity bar — always visible, 60px wide */
|
|
4549
4900
|
!zenMode && React.createElement(
|
|
4550
4901
|
"div",
|
|
4551
4902
|
{ className: "ide-activity-bar" },
|
|
@@ -4688,6 +5039,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4688
5039
|
{
|
|
4689
5040
|
key: tab.id,
|
|
4690
5041
|
className: "tree-item " + (pane.activeTabId === tab.id && state.focusedPaneId === pane.id ? "active" : ""),
|
|
5042
|
+
// The name ellipsises in a narrow sidebar, so the row
|
|
5043
|
+
// carries the full path the way file-tree rows do.
|
|
5044
|
+
title: tab.path || tab.name,
|
|
4691
5045
|
onClick: function () {
|
|
4692
5046
|
if (tab.path && !tab.path.startsWith('mbeditor://') && tab.path !== '__settings__') {
|
|
4693
5047
|
handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
|
|
@@ -4698,17 +5052,24 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4698
5052
|
React.createElement("i", { className: "tree-item-icon " + (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code') + " tree-file-icon" }),
|
|
4699
5053
|
React.createElement(
|
|
4700
5054
|
"div",
|
|
4701
|
-
|
|
5055
|
+
// minWidth:0 on both the row's name cell and the label
|
|
5056
|
+
// itself: without it a flex item refuses to shrink
|
|
5057
|
+
// below its content, so a long filename pushed out
|
|
5058
|
+
// under the (formerly absolute) action buttons.
|
|
5059
|
+
{ className: "tree-item-name", style: { display: 'flex', alignItems: 'center', minWidth: 0 } },
|
|
4702
5060
|
React.createElement(
|
|
4703
5061
|
"span",
|
|
4704
|
-
{ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
5062
|
+
{ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 } },
|
|
4705
5063
|
tab.name
|
|
4706
5064
|
),
|
|
4707
|
-
tab.dirty && React.createElement("i", { className: "fas fa-circle", style: { fontSize: '5px', color: '#e3d286', marginLeft: '6px', marginTop: '1px' } })
|
|
5065
|
+
tab.dirty && React.createElement("i", { className: "fas fa-circle", style: { fontSize: '5px', color: '#e3d286', marginLeft: '6px', marginTop: '1px', flexShrink: 0 } })
|
|
4708
5066
|
),
|
|
4709
5067
|
React.createElement(
|
|
4710
5068
|
"div",
|
|
4711
|
-
|
|
5069
|
+
// In flow, not absolute — the buttons now claim their
|
|
5070
|
+
// own width so the name truncates instead of running
|
|
5071
|
+
// underneath them.
|
|
5072
|
+
{ className: "tab-actions", style: { display: 'flex', alignItems: 'center', flexShrink: 0, marginLeft: 'auto' } },
|
|
4712
5073
|
React.createElement(
|
|
4713
5074
|
"div",
|
|
4714
5075
|
{ className: "tab-split", onClick: function (e) {
|
|
@@ -4720,7 +5081,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4720
5081
|
"div",
|
|
4721
5082
|
{ className: "tab-close", onClick: function (e) {
|
|
4722
5083
|
e.stopPropagation();requestCloseTab(pane.id, tab.id);
|
|
4723
|
-
}, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 }
|
|
5084
|
+
}, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 },
|
|
5085
|
+
// See TabBar: role="button" would pick up Pico's
|
|
5086
|
+
// button skin from the host app and square this off.
|
|
5087
|
+
title: "Close " + tab.name + (tab.dirty ? " (unsaved changes)" : "") },
|
|
4724
5088
|
React.createElement("i", { className: "fas fa-times" })
|
|
4725
5089
|
)
|
|
4726
5090
|
)
|
|
@@ -4780,6 +5144,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4780
5144
|
},
|
|
4781
5145
|
disabled: !!loading.createDir
|
|
4782
5146
|
}),
|
|
5147
|
+
React.createElement(SidebarActionButton, {
|
|
5148
|
+
title: "Upload files",
|
|
5149
|
+
iconClass: 'fas fa-upload',
|
|
5150
|
+
onClick: function () {
|
|
5151
|
+
return openImportDialog(selectedTreeNode);
|
|
5152
|
+
}
|
|
5153
|
+
}),
|
|
4783
5154
|
React.createElement(SidebarActionButton, {
|
|
4784
5155
|
title: "Rename selected",
|
|
4785
5156
|
iconClass: 'fas fa-pen',
|
|
@@ -4802,7 +5173,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4802
5173
|
)
|
|
4803
5174
|
},
|
|
4804
5175
|
React.createElement(FileTree, {
|
|
4805
|
-
items:
|
|
5176
|
+
items: fileTreeItems,
|
|
4806
5177
|
onSelect: handleSoftOpenFile,
|
|
4807
5178
|
activePath: editorPrefs.autoRevealInExplorer !== false ? (activeTab && activeTab.path) : null,
|
|
4808
5179
|
selectedPaths: selectedPaths,
|
|
@@ -4937,6 +5308,30 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4937
5308
|
var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
|
|
4938
5309
|
var hasAny = loadedCount > 0;
|
|
4939
5310
|
|
|
5311
|
+
// Grouped into a VS Code-style tree, then flattened straight back
|
|
5312
|
+
// into one row array: the windowing below is unchanged, it just
|
|
5313
|
+
// indexes rows instead of results. Every tier emits its hits file
|
|
5314
|
+
// by file, so a run of the same path is the whole group.
|
|
5315
|
+
var rows = [];
|
|
5316
|
+
var group = null;
|
|
5317
|
+
allResults.forEach(function (res, idx) {
|
|
5318
|
+
if (!group || group.file !== res.file) {
|
|
5319
|
+
group = { type: 'file', file: res.file, count: 0 };
|
|
5320
|
+
rows.push(group);
|
|
5321
|
+
}
|
|
5322
|
+
group.count += 1;
|
|
5323
|
+
if (!searchCollapsedFiles[res.file]) rows.push({ type: 'match', res: res, idx: idx });
|
|
5324
|
+
});
|
|
5325
|
+
var rowCount = rows.length;
|
|
5326
|
+
|
|
5327
|
+
// Only the rows on screen (plus a small buffer either side) are
|
|
5328
|
+
// built. Everything else is represented by the height of the
|
|
5329
|
+
// spacer, so the scrollbar and every scroll position stay exactly
|
|
5330
|
+
// as they would be for the full list.
|
|
5331
|
+
var winStart = Math.max(0, Math.floor(searchViewport.scrollTop / SEARCH_ROW_HEIGHT) - SEARCH_ROW_BUFFER);
|
|
5332
|
+
var winEnd = Math.min(rowCount, Math.ceil((searchViewport.scrollTop + (searchViewport.height || 600)) / SEARCH_ROW_HEIGHT) + SEARCH_ROW_BUFFER);
|
|
5333
|
+
var visible = rows.slice(winStart, winEnd);
|
|
5334
|
+
|
|
4940
5335
|
return React.createElement(
|
|
4941
5336
|
React.Fragment,
|
|
4942
5337
|
null,
|
|
@@ -4954,32 +5349,65 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4954
5349
|
"div",
|
|
4955
5350
|
{
|
|
4956
5351
|
className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
|
|
4957
|
-
ref:
|
|
5352
|
+
ref: attachSearchResults,
|
|
4958
5353
|
onScroll: handleSearchResultsScroll
|
|
4959
5354
|
},
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5355
|
+
React.createElement(
|
|
5356
|
+
"div",
|
|
5357
|
+
{ style: { height: rowCount * SEARCH_ROW_HEIGHT, position: 'relative' } },
|
|
5358
|
+
visible.map(function(row, vi) {
|
|
5359
|
+
var i = winStart + vi;
|
|
5360
|
+
var top = { position: 'absolute', top: i * SEARCH_ROW_HEIGHT, left: 0, right: 0 };
|
|
5361
|
+
|
|
5362
|
+
if (row.type === 'file') {
|
|
5363
|
+
var fileName = row.file.split('/').pop();
|
|
5364
|
+
var dir = row.file.slice(0, row.file.length - fileName.length).replace(/\/$/, '');
|
|
5365
|
+
var collapsed = !!searchCollapsedFiles[row.file];
|
|
5366
|
+
return React.createElement(
|
|
5367
|
+
"div",
|
|
5368
|
+
{
|
|
5369
|
+
key: 'f:' + row.file,
|
|
5370
|
+
className: "search-result-file-row",
|
|
5371
|
+
style: top,
|
|
5372
|
+
title: row.file,
|
|
5373
|
+
onClick: (function(f) { return function() { toggleSearchFile(f); }; })(row.file)
|
|
5374
|
+
},
|
|
5375
|
+
React.createElement("i", { className: "codicon codicon-chevron-" + (collapsed ? "right" : "down") + " search-result-chevron" }),
|
|
5376
|
+
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
5377
|
+
React.createElement("span", { className: "search-result-file-name" }, fileName),
|
|
5378
|
+
dir && React.createElement("span", { className: "search-result-file-dir" }, dir),
|
|
5379
|
+
React.createElement("span", { className: "search-result-count" }, row.count)
|
|
5380
|
+
);
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
var res = row.res;
|
|
5384
|
+
return React.createElement(
|
|
5385
|
+
"div",
|
|
5386
|
+
{
|
|
5387
|
+
key: 'm:' + row.idx,
|
|
5388
|
+
className: "search-result-item",
|
|
5389
|
+
style: top,
|
|
5390
|
+
title: res.file + ":" + res.line,
|
|
5391
|
+
// col..end_col selects the match and leaves the cursor
|
|
5392
|
+
// just past it, which is where you want to start
|
|
5393
|
+
// typing after jumping to a hit — not column 1.
|
|
5394
|
+
onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line, r.col || r.end_col, r.end_col); }; })(res)
|
|
5395
|
+
},
|
|
5396
|
+
React.createElement("span", { className: "search-result-line-num" }, res.line),
|
|
5397
|
+
(function () {
|
|
5398
|
+
var parts = searchMatchParts(res);
|
|
5399
|
+
if (!parts) return React.createElement("span", { className: "search-result-text" }, res.text);
|
|
5400
|
+
return React.createElement(
|
|
5401
|
+
"span",
|
|
5402
|
+
{ className: "search-result-text search-result-text-split" },
|
|
5403
|
+
React.createElement("span", { className: "search-result-pre" }, parts[0]),
|
|
5404
|
+
React.createElement("mark", { className: "search-result-match" }, parts[1]),
|
|
5405
|
+
React.createElement("span", { className: "search-result-post" }, parts[2])
|
|
5406
|
+
);
|
|
5407
|
+
})()
|
|
5408
|
+
);
|
|
5409
|
+
})
|
|
5410
|
+
),
|
|
4983
5411
|
searchHasMore && React.createElement(
|
|
4984
5412
|
"div", { className: "search-loading-more" },
|
|
4985
5413
|
React.createElement("i", { className: "fas fa-spinner fa-spin" }),
|
|
@@ -5573,16 +6001,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5573
6001
|
React.createElement('option', { value: 'allDocuments' }, 'All open files')
|
|
5574
6002
|
)
|
|
5575
6003
|
),
|
|
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
6004
|
React.createElement(
|
|
5587
6005
|
'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
6006
|
React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
|
|
@@ -5767,6 +6185,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5767
6185
|
})
|
|
5768
6186
|
),
|
|
5769
6187
|
|
|
6188
|
+
React.createElement(
|
|
6189
|
+
'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' },
|
|
6190
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Controller route hints'),
|
|
6191
|
+
React.createElement('input', {
|
|
6192
|
+
type: 'checkbox',
|
|
6193
|
+
className: 'ide-settings-checkbox',
|
|
6194
|
+
checked: editorPrefs.routeHints !== false,
|
|
6195
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { routeHints: v }); }); }
|
|
6196
|
+
})
|
|
6197
|
+
),
|
|
6198
|
+
|
|
5770
6199
|
/* ── RuboCop ─────────────────────────────────── */
|
|
5771
6200
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
|
|
5772
6201
|
React.createElement(
|
|
@@ -5798,6 +6227,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5798
6227
|
{
|
|
5799
6228
|
className: 'ide-settings-reset-btn',
|
|
5800
6229
|
type: 'button',
|
|
6230
|
+
title: 'Restore every editor preference on this page to its default',
|
|
5801
6231
|
onClick: function() { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); }
|
|
5802
6232
|
},
|
|
5803
6233
|
React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
|
|
@@ -5824,17 +6254,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5824
6254
|
paneId: pane.id,
|
|
5825
6255
|
markers: markers[pActiveTab.id] || [],
|
|
5826
6256
|
gitAvailable: gitAvailable,
|
|
5827
|
-
testAvailable: testAvailable,
|
|
5828
6257
|
treeData: treeData,
|
|
5829
6258
|
testResult: testResult,
|
|
5830
6259
|
testPanelFile: testPanelFile,
|
|
5831
|
-
testLoading: testLoading,
|
|
5832
6260
|
testInlineVisible: testInlineVisible,
|
|
5833
6261
|
editorPrefs: editorPrefs,
|
|
5834
6262
|
monacoReady: monacoReady,
|
|
5835
6263
|
onFormat: function() { onFormatRef.current(); },
|
|
5836
6264
|
onSave: function() { handleSave(pane.id, pActiveTab); },
|
|
5837
|
-
onRunTest: handleRunTest,
|
|
5838
6265
|
onRunTestAtCursor: handleRunTestAtCursor,
|
|
5839
6266
|
onShowHistory: function(path) { setHistoryPanelPath(path); },
|
|
5840
6267
|
onContentChange: function onContentChange(val) {
|
|
@@ -6018,8 +6445,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6018
6445
|
onClose: function () { setShowProblemsPanel(false); },
|
|
6019
6446
|
onOpenFile: function (path, line, col) {
|
|
6020
6447
|
handleSelectFile(path, path.split('/').pop(), line, col);
|
|
6021
|
-
}
|
|
6022
|
-
|
|
6448
|
+
},
|
|
6449
|
+
// `rubocop -a` writes through a subprocess, so the server's
|
|
6450
|
+
// files_changed broadcast is the only notice — and there is no
|
|
6451
|
+
// broadcast at all without a cable connection. Re-read every open tab
|
|
6452
|
+
// here too: clean tabs take the corrected text (and re-lint), dirty
|
|
6453
|
+
// ones get the usual reload prompt instead of being silently clobbered.
|
|
6454
|
+
onFilesRewritten: function () { checkOpenTabsForExternalChanges(); }
|
|
6455
|
+
}),
|
|
6023
6456
|
),
|
|
6024
6457
|
|
|
6025
6458
|
// Right-side Git panel (children of ide-body, alongside sidebar and ide-main)
|
|
@@ -6079,33 +6512,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6079
6512
|
},
|
|
6080
6513
|
React.createElement("i", { className: "fas fa-bug statusbar-problems-error-icon" }),
|
|
6081
6514
|
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.errors),
|
|
6082
|
-
React.createElement("i", {
|
|
6083
|
-
className: "fas fa-exclamation-triangle statusbar-problems-warning-icon",
|
|
6084
|
-
style: { marginLeft: "8px" }
|
|
6085
|
-
}),
|
|
6515
|
+
React.createElement("i", { className: "fas fa-exclamation-triangle statusbar-problems-warning-icon" }),
|
|
6086
6516
|
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
|
|
6087
6517
|
),
|
|
6088
|
-
// ruby-lsp indicator. Hidden entirely when ruby-lsp was never available
|
|
6089
|
-
// and nothing has gone wrong — a permanent "off" badge in a project with
|
|
6090
|
-
// no Ruby is noise. A healthy server gets a quiet icon; a degraded one
|
|
6091
|
-
// gets an amber chip you can click to restart.
|
|
6092
|
-
(lspHealth.status !== 'off' || lspHealth.reason) && React.createElement(
|
|
6093
|
-
"button",
|
|
6094
|
-
{
|
|
6095
|
-
type: "button",
|
|
6096
|
-
className: "statusbar-btn statusbar-lsp statusbar-lsp-" + lspHealth.status,
|
|
6097
|
-
onClick: restartRubyLsp,
|
|
6098
|
-
title: lspHealth.status === 'ok'
|
|
6099
|
-
? 'ruby-lsp is running — click to restart'
|
|
6100
|
-
: 'ruby-lsp unavailable' + (lspHealth.reason ? ': ' + lspHealth.reason : '') +
|
|
6101
|
-
'. Falling back to search-based lookups. Click to retry.'
|
|
6102
|
-
},
|
|
6103
|
-
React.createElement("i", {
|
|
6104
|
-
className: "fas " + (lspHealth.status === 'ok' ? 'fa-gem' : 'fa-plug'),
|
|
6105
|
-
"aria-hidden": "true"
|
|
6106
|
-
}),
|
|
6107
|
-
lspHealth.status !== 'ok' && React.createElement("span", null, " ruby-lsp")
|
|
6108
|
-
),
|
|
6109
6518
|
!serverOnline && (function () {
|
|
6110
6519
|
var dirtyCount = state.panes.reduce(function (acc, p) {
|
|
6111
6520
|
return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
|
|
@@ -6424,6 +6833,23 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6424
6833
|
" Delete"
|
|
6425
6834
|
),
|
|
6426
6835
|
React.createElement("div", { className: "context-menu-divider" }),
|
|
6836
|
+
contextMenu.node && contextMenu.node.type === 'file' && React.createElement(
|
|
6837
|
+
"div",
|
|
6838
|
+
{ className: "context-menu-item", onClick: function () {
|
|
6839
|
+
return handleContextMenuAction('download');
|
|
6840
|
+
} },
|
|
6841
|
+
React.createElement("i", { className: "fas fa-download context-menu-icon" }),
|
|
6842
|
+
" Download"
|
|
6843
|
+
),
|
|
6844
|
+
React.createElement(
|
|
6845
|
+
"div",
|
|
6846
|
+
{ className: "context-menu-item", onClick: function () {
|
|
6847
|
+
return handleContextMenuAction('upload');
|
|
6848
|
+
} },
|
|
6849
|
+
React.createElement("i", { className: "fas fa-upload context-menu-icon" }),
|
|
6850
|
+
" Upload Files Here..."
|
|
6851
|
+
),
|
|
6852
|
+
React.createElement("div", { className: "context-menu-divider" }),
|
|
6427
6853
|
React.createElement(
|
|
6428
6854
|
"div",
|
|
6429
6855
|
{ className: "context-menu-item", onClick: function () {
|
|
@@ -6499,6 +6925,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6499
6925
|
)
|
|
6500
6926
|
),
|
|
6501
6927
|
|
|
6928
|
+
/* ── Upload dialog ─────────────────────────────────────────────────── */
|
|
6929
|
+
importDialog && React.createElement(ImportDialog, {
|
|
6930
|
+
initialFolder: importDialog.initialFolder,
|
|
6931
|
+
docs: SearchService.allDocs(),
|
|
6932
|
+
onCancel: function () { setImportDialog(null); },
|
|
6933
|
+
onImport: confirmImportDialog
|
|
6934
|
+
}),
|
|
6935
|
+
|
|
6502
6936
|
/* ── Import conflict modal ─────────────────────────────────────────── */
|
|
6503
6937
|
importConflict && React.createElement(ImportConflictModal, {
|
|
6504
6938
|
conflicts: importConflict.result.conflicts,
|