mbeditor 0.12.7 → 0.12.9
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 +103 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +48 -23
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +139 -50
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +56 -7
- data/app/assets/javascripts/mbeditor/editor_plugins.js +74 -92
- data/app/assets/javascripts/mbeditor/tab_manager.js +34 -0
- data/app/assets/stylesheets/mbeditor/editor.css +34 -2
- data/app/controllers/mbeditor/editors_controller.rb +14 -1
- data/app/services/mbeditor/js_syntax_check_service.rb +212 -0
- data/app/services/mbeditor/search_replace_service.rb +49 -8
- data/lib/mbeditor/configuration.rb +2 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 264dfef604baf70be3fcb9984f588a93a8388fe13371cb3c8707d5ee7149d441
|
|
4
|
+
data.tar.gz: d584056fbb108ea55ddb98a819af73e4eabb939d659fc7d54442446b34b2c203
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20141c5dcd65e482c4d0437c5b1a4b1431507f424dc1a31b80af5490d30b4611c2c2e050f0716a4a382c9f2b3804001127e03292cfe371b50049c6d058f3c7b0
|
|
7
|
+
data.tar.gz: 551f8ad767679d4b63364d2c6e9bcb267663c35bf1750a355b29ff22f7149e445511a0fd9be8aa7a279754f6c0a3ccb79a1b3941d1bec3b11792911541081616
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,109 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.9] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Problems panel severity filter.** Three toggle chips — errors, warnings,
|
|
14
|
+
info — each showing its live count, filtering the list independently of the
|
|
15
|
+
text filter. Multi-select rather than one-of-three, so "errors and warnings
|
|
16
|
+
but not the convention noise" is expressible. The selection persists, and the
|
|
17
|
+
last active severity stays latched on so the panel can't be filtered down to
|
|
18
|
+
an unexplained blank.
|
|
19
|
+
- **Search results carry match columns.** Clicking a result now puts the cursor
|
|
20
|
+
at the end of the matched text instead of the start of the line, ready to
|
|
21
|
+
type. Columns are measured against the raw source line on the server —
|
|
22
|
+
the row's display text is stripped, so a column derived from it would land
|
|
23
|
+
short by the indent on every indented hit.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **The Problems header no longer restates its counts in prose.** The severity
|
|
27
|
+
chips carry the same three numbers.
|
|
28
|
+
- **Paste re-indents instead of reformatting.** Pasting ran the pasted range
|
|
29
|
+
through Prettier, which reprinted the whole enclosing statement — and for a
|
|
30
|
+
paste that filled the document, every line of it — so pasting a snippet
|
|
31
|
+
rewrote code you never touched. Paste now only re-indents to the file's
|
|
32
|
+
indentation setting; quotes, semicolons and spacing come through exactly as
|
|
33
|
+
copied. The setting is renamed "Format on paste" → "Indent on paste"
|
|
34
|
+
(`indentOnPaste`, still on by default). Monaco ships no indentation rules for
|
|
35
|
+
JavaScript, which would have made the re-indent a silent no-op there, so
|
|
36
|
+
VS Code's JS rules are now registered. Format-on-save is unchanged and still
|
|
37
|
+
off by default.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **Go-to-definition on a JS symbol defined in the same file opened a junk tab
|
|
41
|
+
named after a number.** Models are created without an explicit URI, so Monaco
|
|
42
|
+
identifies each as `inmemory://model/N`; the TS worker returns that URI for
|
|
43
|
+
an in-file definition, and the editor opener stripped it to a path and opened
|
|
44
|
+
a phantom tab called "57". Only `file://` resources are treated as workspace
|
|
45
|
+
files now — the rest go back to Monaco, which reveals the position in the
|
|
46
|
+
current editor.
|
|
47
|
+
- **Clicking a search result sometimes landed on the wrong line.** Opening a
|
|
48
|
+
hit in a file that wasn't already open ran the jump against a still-empty
|
|
49
|
+
model: it clamped to line 1 and then cleared the pending jump, so when the
|
|
50
|
+
content finally arrived nothing re-triggered it. Whether it misbehaved came
|
|
51
|
+
down to whether the fetch beat a 50 ms timer — hence "sometimes". The jump
|
|
52
|
+
now waits for the content, cancels a superseded timer, and clamps to the
|
|
53
|
+
file's real length when a result outlives the line it pointed at.
|
|
54
|
+
- **Search results ignored created and deleted files.** Only saves dropped the
|
|
55
|
+
client-side search cache, so a search re-run after adding or removing a file
|
|
56
|
+
was served the stale cached page. Every structural mutation now invalidates
|
|
57
|
+
it and re-runs the active query.
|
|
58
|
+
- **Search and the git status counts lagged behind file changes.** The
|
|
59
|
+
live-result refresh sat behind a 2 s debounce (now 250 ms), and every save
|
|
60
|
+
fired the full `/git_info` fan-out — the most expensive request the editor
|
|
61
|
+
makes — twice over, once directly and once from the broadcast handler, which
|
|
62
|
+
on a dev server with a few threads queued the tree and search requests behind
|
|
63
|
+
it. Saves and file mutations now use the cheap `/git_status` probe, which
|
|
64
|
+
patches the branch and file list immediately and escalates to the fan-out
|
|
65
|
+
itself only when the branch actually changed.
|
|
66
|
+
|
|
67
|
+
## [0.12.8] - 2026-08-07
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
- **Untitled scratch tabs.** The tab bar's "+" now opens an in-memory
|
|
71
|
+
`Untitled-N` buffer (VS Code-style) instead of prompting to create a file on
|
|
72
|
+
disk. Nothing is written anywhere until you save, at which point a save-as
|
|
73
|
+
prompt asks for a workspace-relative path and the tab converts to a real
|
|
74
|
+
file. Scratch tabs are skipped by Save All (each needs its own prompt) and
|
|
75
|
+
are not persisted across reloads.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- **"was updated externally" fired after your own saves.** A successful save
|
|
79
|
+
never refreshed the external-change baseline (the save-time grace window
|
|
80
|
+
skipped the very fetch that would have), so once you edited the file again,
|
|
81
|
+
the next save of *any* file compared new disk content against the stale
|
|
82
|
+
pre-save baseline and raised the banner. Saves now update the baseline
|
|
83
|
+
directly, the check re-reads live tab state instead of a snapshot taken
|
|
84
|
+
before its fetch, and — since the files_changed push only ever announces
|
|
85
|
+
mbeditor's own writes — the push-triggered check is now scoped to the pushed
|
|
86
|
+
paths instead of re-fetching every open tab on every save. The manual
|
|
87
|
+
Refresh Workspace button still checks everything.
|
|
88
|
+
- **Virtual tabs no longer poll git.** Changelog/untitled/diff tabs were
|
|
89
|
+
fetching git line-diff every 10 s and git/file history on focus — guaranteed
|
|
90
|
+
no-ops, now skipped, along with persistent-undo tracking for paths that have
|
|
91
|
+
no file behind them.
|
|
92
|
+
- **git-tier search returned nothing, instantly, on hosts with older git.** The
|
|
93
|
+
exclusion pathspecs added in 0.12.6 produced a pathspec list of nothing but
|
|
94
|
+
`:(exclude)` entries. Newer git reads that as "everything except these";
|
|
95
|
+
older git refuses it ("fatal: There is nothing to exclude from"), exits 128,
|
|
96
|
+
and — with stderr discarded — search silently returned empty. The list is
|
|
97
|
+
now anchored with a `.` pathspec, which every git version accepts.
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
- **Babel-based scope lint for JS/JSX, surfaced as warnings on save.** On top
|
|
101
|
+
of the existing babel syntax check (host mini_racer + babel-standalone), the
|
|
102
|
+
saved file is now traversed for identifier references that bind to nothing:
|
|
103
|
+
not to any scope in the file, not to a top-level declaration in any other
|
|
104
|
+
workspace JS file (Sprockets concatenates them into one scope), not to a
|
|
105
|
+
known `window.X` global, and not to the browser/React/hook names. Each one
|
|
106
|
+
gets a warning marker — `'name' is not defined in any reachable scope` — in
|
|
107
|
+
the editor and Problems panel. Also warns on bindings that are only assigned
|
|
108
|
+
inside a `useEffect`/`useLayoutEffect` callback but read during render
|
|
109
|
+
(undefined on first render). Report-only; `config.js_scope_lint = false`
|
|
110
|
+
disables it. Requires a babel-standalone new enough to expose
|
|
111
|
+
`Babel.packages` (7.9+); older bundles degrade to the syntax check alone.
|
|
112
|
+
|
|
10
113
|
## [0.12.7] - 2026-08-06
|
|
11
114
|
|
|
12
115
|
### Fixed
|
|
@@ -603,7 +603,11 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
603
603
|
CollaborationService.ensureRoom(tab.path);
|
|
604
604
|
collabActiveRef.current = _collabActive;
|
|
605
605
|
|
|
606
|
-
|
|
606
|
+
// untitled://, mbeditor:// and friends have no file to persist history
|
|
607
|
+
// against — tracking them just produces doomed /file_history requests.
|
|
608
|
+
var _virtualPath = (tab.path || '').indexOf('://') >= 0;
|
|
609
|
+
|
|
610
|
+
if (!_collabActive && !_virtualPath && typeof HistoryService !== 'undefined') {
|
|
607
611
|
var _histBranch = EditorStore.getState().gitBranch || '';
|
|
608
612
|
if (_histBranch) {
|
|
609
613
|
if (_reusingModel) {
|
|
@@ -643,9 +647,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
643
647
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
644
648
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
645
649
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
646
|
-
// Monaco
|
|
647
|
-
// here. attachEditorFeatures drives
|
|
648
|
-
//
|
|
650
|
+
// Monaco own format-on-paste is left off: it never ran the formatter
|
|
651
|
+
// here. attachEditorFeatures drives re-indent-on-paste from onDidPaste
|
|
652
|
+
// instead, gated on editorPrefs.indentOnPaste.
|
|
649
653
|
formatOnPaste: false,
|
|
650
654
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
651
655
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -894,7 +898,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
894
898
|
// Phase 2: background undo-history replay.
|
|
895
899
|
// Only run for newly-created models (reused models already have their undo stack).
|
|
896
900
|
var _phase2CleanupFn = null;
|
|
897
|
-
if (!_collabActive && !_reusingModel && typeof HistoryService !== 'undefined') {
|
|
901
|
+
if (!_collabActive && !_virtualPath && !_reusingModel && typeof HistoryService !== 'undefined') {
|
|
898
902
|
var _phase2Branch = EditorStore.getState().gitBranch || '';
|
|
899
903
|
var _phase2Path = tab.path;
|
|
900
904
|
var _phase2Content = tab.content || '';
|
|
@@ -1209,9 +1213,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1209
1213
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
1210
1214
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
1211
1215
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
1212
|
-
// Monaco
|
|
1213
|
-
// here. attachEditorFeatures drives
|
|
1214
|
-
//
|
|
1216
|
+
// Monaco own format-on-paste is left off: it never ran the formatter
|
|
1217
|
+
// here. attachEditorFeatures drives re-indent-on-paste from onDidPaste
|
|
1218
|
+
// instead, gated on editorPrefs.indentOnPaste.
|
|
1215
1219
|
formatOnPaste: false,
|
|
1216
1220
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1217
1221
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -1297,22 +1301,40 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1297
1301
|
return function() { window.removeEventListener('mbeditor:focusPane', onFocusPane); };
|
|
1298
1302
|
}, [paneId]);
|
|
1299
1303
|
|
|
1300
|
-
// Jump to line if specified
|
|
1304
|
+
// Jump to line if specified.
|
|
1305
|
+
//
|
|
1306
|
+
// The wait for content is load-bearing, not defensive. Opening a search hit
|
|
1307
|
+
// in a file that wasn't already open runs this effect once while the model
|
|
1308
|
+
// is still empty: the jump clamped to line 1 and then cleared gotoLine, so
|
|
1309
|
+
// when the content finally arrived there was nothing left to re-trigger it
|
|
1310
|
+
// and the cursor stayed on the wrong line. Whether it misbehaved came down
|
|
1311
|
+
// to whether the fetch beat a 50 ms timer — hence "sometimes".
|
|
1301
1312
|
useEffect(function () {
|
|
1302
|
-
if (tab.gotoLine
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1313
|
+
if (!tab.gotoLine || !monacoRef.current) return;
|
|
1314
|
+
if (tab.loading) return;
|
|
1315
|
+
|
|
1316
|
+
var editor = monacoRef.current;
|
|
1317
|
+
var timer = setTimeout(function () {
|
|
1318
|
+
var model = editor.getModel();
|
|
1319
|
+
if (!model || model.isDisposed()) return;
|
|
1320
|
+
|
|
1321
|
+
// A result can outlive the line it pointed at (the file shrank since the
|
|
1322
|
+
// scan). Clamp instead of asking Monaco for a line that isn't there.
|
|
1323
|
+
var line = Math.max(1, Math.min(tab.gotoLine, model.getLineCount()));
|
|
1324
|
+
var column = tab.gotoCol || 1;
|
|
1325
|
+
var maxColumn = model.getLineMaxColumn(line);
|
|
1326
|
+
if (column > maxColumn) column = maxColumn;
|
|
1327
|
+
|
|
1328
|
+
editor.revealLineInCenter(line);
|
|
1329
|
+
editor.setPosition({ lineNumber: line, column: column });
|
|
1330
|
+
editor.focus();
|
|
1331
|
+
|
|
1332
|
+
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
1333
|
+
TabManager.clearGotoLine(paneId, tab.path);
|
|
1334
|
+
}, 50);
|
|
1335
|
+
|
|
1336
|
+
return function () { clearTimeout(timer); };
|
|
1337
|
+
}, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
|
|
1316
1338
|
|
|
1317
1339
|
// Apply RuboCop markers
|
|
1318
1340
|
useEffect(function () {
|
|
@@ -1410,7 +1432,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1410
1432
|
var GIT_LINE_POLL_MS = 10000;
|
|
1411
1433
|
|
|
1412
1434
|
useEffect(function () {
|
|
1435
|
+
// Virtual tabs (untitled://, mbeditor://, diff views) have no file behind
|
|
1436
|
+
// them — polling git for one is a guaranteed-404 request every 10s.
|
|
1413
1437
|
if (!gitAvailable || !tab.path || tab.isDiff || tab.isCombinedDiff) return;
|
|
1438
|
+
if (tab.path.indexOf('://') >= 0) return;
|
|
1414
1439
|
|
|
1415
1440
|
var cancelled = false;
|
|
1416
1441
|
|
|
@@ -109,7 +109,7 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
109
109
|
autoClosingBrackets: 'always',
|
|
110
110
|
autoClosingQuotes: 'always',
|
|
111
111
|
autoIndent: 'full',
|
|
112
|
-
|
|
112
|
+
indentOnPaste: true,
|
|
113
113
|
formatOnType: false,
|
|
114
114
|
formatOnSave: false,
|
|
115
115
|
quickSuggestions: true,
|
|
@@ -744,6 +744,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
744
744
|
// buffer flags every dirty tab, which is just the definition of "dirty".
|
|
745
745
|
var lastDiskContentRef = useRef({});
|
|
746
746
|
|
|
747
|
+
// Every successful save must go through this. Besides the 3.5s grace window
|
|
748
|
+
// that stops the external-change check racing our own write, a save defines
|
|
749
|
+
// the new on-disk truth — so it must also refresh the external-change
|
|
750
|
+
// baseline. The check's own fetch is skipped inside the grace window, so
|
|
751
|
+
// without this the baseline stayed at the *pre-save* disk content and the
|
|
752
|
+
// next files_changed push reported our own save as an external edit on any
|
|
753
|
+
// tab the user had started editing again.
|
|
754
|
+
function noteLocalSave(path, content) {
|
|
755
|
+
recentSavesRef.current[path] = Date.now();
|
|
756
|
+
setTimeout(function () { delete recentSavesRef.current[path]; }, 3500);
|
|
757
|
+
if (typeof content === 'string') {
|
|
758
|
+
lastDiskContentRef.current[path] = content.replace(/\r\n/g, '\n');
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
747
762
|
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
748
763
|
var draftWriteTimerRef = useRef({});
|
|
749
764
|
var serverOnlineRef = useRef(true);
|
|
@@ -841,10 +856,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
841
856
|
});
|
|
842
857
|
};
|
|
843
858
|
|
|
859
|
+
// Every structural mutation (create, delete, rename, import) funnels through
|
|
860
|
+
// here, so this is where the project-search cache has to be dropped. Saves
|
|
861
|
+
// invalidated it at their own call sites, which is why editing a file
|
|
862
|
+
// updated the results but adding or deleting one never did — the stale
|
|
863
|
+
// cached page was served for the same query. The per-path delta refresh on
|
|
864
|
+
// the files_changed push can't cover it either: it re-scans named files,
|
|
865
|
+
// and a file that just appeared or vanished isn't in the previous result set.
|
|
844
866
|
var refreshProjectTree = function refreshProjectTree() {
|
|
845
867
|
return FileService.getTree().then(function (data) {
|
|
846
868
|
setTreeData(data || []);
|
|
847
869
|
SearchService.buildIndex(data || []);
|
|
870
|
+
SearchService.invalidate();
|
|
871
|
+
if (searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
872
|
+
_debouncedSearch(searchQueryRef.current);
|
|
873
|
+
}
|
|
848
874
|
return data || [];
|
|
849
875
|
})["catch"](function (err) {
|
|
850
876
|
EditorStore.setStatus("Failed to refresh files: " + (err && err.message || "Unknown error"), "error");
|
|
@@ -1319,7 +1345,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1319
1345
|
return {
|
|
1320
1346
|
id: p.id,
|
|
1321
1347
|
activeTabId: p.activeTabId,
|
|
1322
|
-
tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isModelGraph; }).map(function (t) {
|
|
1348
|
+
tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isModelGraph && !t.isUntitled; }).map(function (t) {
|
|
1323
1349
|
return {
|
|
1324
1350
|
id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
|
|
1325
1351
|
isSettings: !!t.isSettings, isPreview: !!t.isPreview, previewFor: t.previewFor || null,
|
|
@@ -1648,17 +1674,28 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1648
1674
|
});
|
|
1649
1675
|
});
|
|
1650
1676
|
}, Promise.resolve());
|
|
1651
|
-
|
|
1677
|
+
// 250ms, not 2s. The window only exists to coalesce the paths from a
|
|
1678
|
+
// burst of saves; anything longer is dead time the user spends looking at
|
|
1679
|
+
// stale search rows, and the server-side result cache was already dropped
|
|
1680
|
+
// by the same broadcast, so waiting buys nothing.
|
|
1681
|
+
}, 250)).current;
|
|
1652
1682
|
|
|
1653
1683
|
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1654
1684
|
// and git status immediately (same work as the 10s poll below does).
|
|
1655
1685
|
useEffect(function () {
|
|
1656
1686
|
function handleFilesChanged(payload) {
|
|
1657
1687
|
if (document.hidden) return;
|
|
1658
|
-
|
|
1688
|
+
// The cheap /git_status probe, not the full /git_info fan-out. This
|
|
1689
|
+
// fires on every save, and the fan-out is the most expensive request
|
|
1690
|
+
// the editor makes — on a dev server with a handful of threads it
|
|
1691
|
+
// queues the tree and search requests behind itself, which is what made
|
|
1692
|
+
// search look like it was waiting for git. fetchStatusLite patches the
|
|
1693
|
+
// branch and file list immediately and escalates to the fan-out on its
|
|
1694
|
+
// own when the branch actually changed.
|
|
1695
|
+
GitService.fetchStatusLite({ background: true })["catch"](function () {});
|
|
1659
1696
|
FileService.getTree().then(function (data) {
|
|
1660
1697
|
setTreeData(_treeUpdater(data || []));
|
|
1661
|
-
checkOpenTabsForExternalChanges();
|
|
1698
|
+
checkOpenTabsForExternalChanges(payload && payload.paths);
|
|
1662
1699
|
})["catch"](function () {});
|
|
1663
1700
|
if (payload && payload.paths && searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
1664
1701
|
payload.paths.forEach(function (p) { _pendingSearchRefreshPaths.current.add(p); });
|
|
@@ -1694,6 +1731,16 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1694
1731
|
});
|
|
1695
1732
|
if (changed) EditorStore.setState({ panes: newPanes });
|
|
1696
1733
|
|
|
1734
|
+
// The CRDT kept our buffer identical to what the peer just wrote, so the
|
|
1735
|
+
// buffer is the new on-disk truth — refresh the external-change baseline.
|
|
1736
|
+
var _savedTab = null;
|
|
1737
|
+
newPanes.forEach(function (p) {
|
|
1738
|
+
p.tabs.forEach(function (t) { if (t.path === path) _savedTab = t; });
|
|
1739
|
+
});
|
|
1740
|
+
if (_savedTab && typeof _savedTab.content === 'string') {
|
|
1741
|
+
lastDiskContentRef.current[path] = _savedTab.content.replace(/\r\n/g, '\n');
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1697
1744
|
// Reset the AVI clean baseline so undo past this peer's save shows dirty correctly.
|
|
1698
1745
|
var _modelEntry = window.__mbeditorModels && window.__mbeditorModels[path];
|
|
1699
1746
|
if (_modelEntry && _modelEntry.model && !_modelEntry.model.isDisposed()) {
|
|
@@ -1704,17 +1751,31 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1704
1751
|
return function () { WebSocketService.offFileSaved(handleFileSaved); };
|
|
1705
1752
|
}, []);
|
|
1706
1753
|
|
|
1707
|
-
|
|
1754
|
+
// onlyPaths: when the trigger names the files that changed (the
|
|
1755
|
+
// files_changed push always does — it only ever announces mbeditor's own
|
|
1756
|
+
// writes), restrict the check to open tabs on those paths. The old
|
|
1757
|
+
// behaviour re-fetched EVERY open tab on every save: N requests per save,
|
|
1758
|
+
// and each one another chance for a stale comparison to cry "changed
|
|
1759
|
+
// externally". A manual workspace refresh passes nothing and still checks
|
|
1760
|
+
// everything — that is the button's job.
|
|
1761
|
+
function checkOpenTabsForExternalChanges(onlyPaths) {
|
|
1762
|
+
var pathSet = null;
|
|
1763
|
+
if (onlyPaths && onlyPaths.length) {
|
|
1764
|
+
pathSet = {};
|
|
1765
|
+
onlyPaths.forEach(function (p) { pathSet[p] = true; });
|
|
1766
|
+
}
|
|
1708
1767
|
var st = EditorStore.getState();
|
|
1709
1768
|
var allTabs = st.panes.reduce(function (acc, p) {
|
|
1710
1769
|
return acc.concat(p.tabs.map(function (t) { return { paneId: p.id, tab: t }; }));
|
|
1711
1770
|
}, []);
|
|
1712
1771
|
var fileTabs = allTabs.filter(function (pt) {
|
|
1713
1772
|
var path = pt.tab.path || '';
|
|
1773
|
+
if (pathSet && !pathSet[path]) return false;
|
|
1714
1774
|
return path &&
|
|
1715
1775
|
!path.startsWith('mbeditor://') &&
|
|
1716
1776
|
!path.startsWith('diff://') &&
|
|
1717
1777
|
!path.startsWith('combined-diff://') &&
|
|
1778
|
+
!path.startsWith('untitled://') &&
|
|
1718
1779
|
!pt.tab.isCombinedDiff &&
|
|
1719
1780
|
!pt.tab.isSettings &&
|
|
1720
1781
|
!pt.tab.isImage &&
|
|
@@ -1736,6 +1797,16 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1736
1797
|
}
|
|
1737
1798
|
FileService.getFile(pt.tab.path, { allowMissing: true }).then(function (data) {
|
|
1738
1799
|
if (!data || typeof data.content !== 'string') return;
|
|
1800
|
+
// Re-read the tab: the snapshot above predates the fetch, and edits or
|
|
1801
|
+
// a save that landed meanwhile would make a stale comparison here
|
|
1802
|
+
// report phantom external changes.
|
|
1803
|
+
var liveState = EditorStore.getState();
|
|
1804
|
+
var livePane = liveState.panes.find(function (p) { return p.id === pt.paneId; });
|
|
1805
|
+
var liveTab = livePane && livePane.tabs.find(function (t) { return t.id === pt.tab.id; });
|
|
1806
|
+
if (!liveTab || liveTab.path !== pt.tab.path) return;
|
|
1807
|
+
var savedAgain = recentSavesRef.current[pt.tab.path];
|
|
1808
|
+
if (savedAgain && Date.now() - savedAgain < 3000) return;
|
|
1809
|
+
pt = { paneId: pt.paneId, tab: liveTab };
|
|
1739
1810
|
var serverNorm = data.content.replace(/\r\n/g, '\n');
|
|
1740
1811
|
var tabNorm = (pt.tab.content || '').replace(/\r\n/g, '\n');
|
|
1741
1812
|
|
|
@@ -1933,17 +2004,29 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1933
2004
|
}
|
|
1934
2005
|
|
|
1935
2006
|
if (save) {
|
|
2007
|
+
if (tab.isUntitled) {
|
|
2008
|
+
// Save-as converts the scratch tab to a real one (closing the scratch
|
|
2009
|
+
// tab in the process); a cancelled prompt keeps the tab open.
|
|
2010
|
+
saveUntitledTab(closingPaneId, tab, { close: true })["catch"](function (err) {
|
|
2011
|
+
if (!(err && err.cancelled)) {
|
|
2012
|
+
EditorStore.setStatus("Save failed: " + (err && err.message || err), "error");
|
|
2013
|
+
}
|
|
2014
|
+
})["finally"](function () {
|
|
2015
|
+
setClosingTabId(null);
|
|
2016
|
+
setClosingPaneId(null);
|
|
2017
|
+
});
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
1936
2020
|
setLoading(function (prev) {
|
|
1937
2021
|
return _extends({}, prev, { save: true });
|
|
1938
2022
|
});
|
|
1939
2023
|
EditorStore.setStatus("Saving " + tab.name + "...", "info");
|
|
1940
2024
|
isSavingRef.current = true;
|
|
1941
2025
|
FileService.saveFile(tab.path, tab.content).then(function () {
|
|
1942
|
-
|
|
1943
|
-
setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
|
|
2026
|
+
noteLocalSave(tab.path, tab.content);
|
|
1944
2027
|
EditorStore.setStatus("Saved", "success");
|
|
1945
2028
|
SearchService.invalidate();
|
|
1946
|
-
GitService.
|
|
2029
|
+
GitService.fetchStatusLite({ background: true });
|
|
1947
2030
|
// Reset the AVI clean baseline so undo past this save point shows dirty correctly.
|
|
1948
2031
|
var _closeEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
1949
2032
|
if (_closeEntry && _closeEntry.model && !_closeEntry.model.isDisposed()) {
|
|
@@ -2037,26 +2120,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2037
2120
|
EditorStore.setStatus("Closed " + saved.length + " saved editor" + (saved.length === 1 ? "" : "s"), "info");
|
|
2038
2121
|
};
|
|
2039
2122
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
var
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2123
|
+
// Save-as for an untitled scratch tab: ask for a workspace-relative path,
|
|
2124
|
+
// write it, then swap the scratch tab for a real one opened at that path.
|
|
2125
|
+
// Returns a promise that rejects with {cancelled: true} when the user backs
|
|
2126
|
+
// out, so close-flows can abort instead of discarding.
|
|
2127
|
+
var saveUntitledTab = function saveUntitledTab(paneId, tab, opts) {
|
|
2128
|
+
var input = window.prompt('Save as (path relative to workspace root):', tab.name + '.txt');
|
|
2129
|
+
if (!input || !input.trim()) return Promise.reject({ cancelled: true });
|
|
2130
|
+
var newPath = input.trim().replace(/^\/+/, '');
|
|
2131
|
+
return FileService.saveFile(newPath, tab.content).then(function () {
|
|
2132
|
+
noteLocalSave(newPath, tab.content);
|
|
2133
|
+
SearchService.invalidate();
|
|
2134
|
+
GitService.fetchStatusLite({ background: true });
|
|
2135
|
+
FileService.getTree().then(function (data) { setTreeData(_treeUpdater(data || [])); })["catch"](function () {});
|
|
2136
|
+
TabManager.closeTab(paneId, tab.id);
|
|
2137
|
+
if (!(opts && opts.close)) {
|
|
2138
|
+
TabManager.openTab(newPath, newPath.split('/').pop(), null, paneId);
|
|
2055
2139
|
}
|
|
2056
|
-
|
|
2057
|
-
}
|
|
2058
|
-
setPendingRename(null);
|
|
2059
|
-
setPendingCreate({ type: 'file', parentPath: baseDir });
|
|
2140
|
+
EditorStore.setStatus('Saved ' + newPath, 'success');
|
|
2141
|
+
});
|
|
2060
2142
|
};
|
|
2061
2143
|
|
|
2062
2144
|
// Persist state when panes, focusedPaneId, or collapsedSections changes
|
|
@@ -2070,7 +2152,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2070
2152
|
return {
|
|
2071
2153
|
id: p.id,
|
|
2072
2154
|
activeTabId: p.activeTabId,
|
|
2073
|
-
tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isModelGraph; }).map(function (t) {
|
|
2155
|
+
tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isModelGraph && !t.isUntitled; }).map(function (t) {
|
|
2074
2156
|
return {
|
|
2075
2157
|
id: t.id,
|
|
2076
2158
|
path: t.path,
|
|
@@ -2566,7 +2648,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2566
2648
|
var setActiveEOL = _useState31e2[1];
|
|
2567
2649
|
|
|
2568
2650
|
useEffect(function () {
|
|
2569
|
-
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('
|
|
2651
|
+
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
|
|
2570
2652
|
setActiveFileCommit(null);
|
|
2571
2653
|
return;
|
|
2572
2654
|
}
|
|
@@ -2711,14 +2793,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2711
2793
|
};
|
|
2712
2794
|
|
|
2713
2795
|
var _doSave = function _doSave(paneId, tab) {
|
|
2796
|
+
if (tab.isUntitled) {
|
|
2797
|
+
saveUntitledTab(paneId, tab)["catch"](function (err) {
|
|
2798
|
+
if (err && err.cancelled) return;
|
|
2799
|
+
EditorStore.setStatus("Save failed: " + (err && err.message || err), "error");
|
|
2800
|
+
});
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2714
2803
|
setLoading(function (prev) {
|
|
2715
2804
|
return _extends({}, prev, { save: true });
|
|
2716
2805
|
});
|
|
2717
2806
|
EditorStore.setStatus("Saving " + tab.name + "...", "info");
|
|
2718
2807
|
isSavingRef.current = true;
|
|
2719
2808
|
FileService.saveFile(tab.path, tab.content).then(function () {
|
|
2720
|
-
|
|
2721
|
-
setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
|
|
2809
|
+
noteLocalSave(tab.path, tab.content);
|
|
2722
2810
|
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
2723
2811
|
if (p.id === paneId) {
|
|
2724
2812
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
@@ -2770,7 +2858,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2770
2858
|
})["catch"](function () {});
|
|
2771
2859
|
}
|
|
2772
2860
|
|
|
2773
|
-
GitService.
|
|
2861
|
+
GitService.fetchStatusLite({ background: true });
|
|
2774
2862
|
})["catch"](function (err) {
|
|
2775
2863
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2776
2864
|
})["finally"](function () {
|
|
@@ -2796,8 +2884,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2796
2884
|
if (!tab) { dismissPendingReload(reload); return; }
|
|
2797
2885
|
isSavingRef.current = true;
|
|
2798
2886
|
FileService.saveFile(tab.path, tab.content).then(function () {
|
|
2799
|
-
|
|
2800
|
-
setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
|
|
2887
|
+
noteLocalSave(tab.path, tab.content);
|
|
2801
2888
|
EditorStore.setState({
|
|
2802
2889
|
panes: EditorStore.getState().panes.map(function (p) {
|
|
2803
2890
|
if (p.id !== reload.paneId) return p;
|
|
@@ -2848,7 +2935,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2848
2935
|
var dirtyTabs = state.panes.flatMap(function (p) {
|
|
2849
2936
|
return p.tabs;
|
|
2850
2937
|
}).filter(function (t) {
|
|
2851
|
-
|
|
2938
|
+
// Untitled scratch tabs need a save-as prompt each — Ctrl+S them
|
|
2939
|
+
// individually; bulk-save skips them rather than stacking prompts.
|
|
2940
|
+
return t.dirty && !t.isUntitled;
|
|
2852
2941
|
});
|
|
2853
2942
|
if (dirtyTabs.length === 0) return;
|
|
2854
2943
|
|
|
@@ -2861,10 +2950,8 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2861
2950
|
return FileService.saveFile(tab.path, tab.content);
|
|
2862
2951
|
});
|
|
2863
2952
|
Promise.all(promises).then(function () {
|
|
2864
|
-
var now = Date.now();
|
|
2865
2953
|
dirtyTabs.forEach(function(tab) {
|
|
2866
|
-
|
|
2867
|
-
setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
|
|
2954
|
+
noteLocalSave(tab.path, tab.content);
|
|
2868
2955
|
});
|
|
2869
2956
|
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
2870
2957
|
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
@@ -2882,7 +2969,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2882
2969
|
});
|
|
2883
2970
|
EditorStore.setStatus("All files saved", "success");
|
|
2884
2971
|
SearchService.invalidate();
|
|
2885
|
-
GitService.
|
|
2972
|
+
GitService.fetchStatusLite({ background: true });
|
|
2886
2973
|
})["catch"](function (err) {
|
|
2887
2974
|
EditorStore.setStatus("Failed to save some files", "error");
|
|
2888
2975
|
})["finally"](function () {
|
|
@@ -3807,7 +3894,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3807
3894
|
EditorStore.setStatus('Created file: ' + createdName, 'success');
|
|
3808
3895
|
return refreshProjectTree().then(function () {
|
|
3809
3896
|
handleSelectFile(createdPath, createdName);
|
|
3810
|
-
GitService.
|
|
3897
|
+
GitService.fetchStatusLite({ background: true });
|
|
3811
3898
|
});
|
|
3812
3899
|
})["catch"](function (err) {
|
|
3813
3900
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3828,7 +3915,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3828
3915
|
handleNodeSelect({ path: createdPath, name: createdPath.split('/').pop(), type: 'folder' });
|
|
3829
3916
|
EditorStore.setStatus('Created folder: ' + createdPath, 'success');
|
|
3830
3917
|
return refreshProjectTree().then(function () {
|
|
3831
|
-
return GitService.
|
|
3918
|
+
return GitService.fetchStatusLite({ background: true });
|
|
3832
3919
|
});
|
|
3833
3920
|
})["catch"](function (err) {
|
|
3834
3921
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3903,7 +3990,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3903
3990
|
setSelectedPaths(new Set([renamedPath]));
|
|
3904
3991
|
EditorStore.setStatus('Renamed to: ' + renamedPath, 'success');
|
|
3905
3992
|
return refreshProjectTree().then(function () {
|
|
3906
|
-
GitService.
|
|
3993
|
+
GitService.fetchStatusLite({ background: true });
|
|
3907
3994
|
});
|
|
3908
3995
|
})["catch"](function (err) {
|
|
3909
3996
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3964,7 +4051,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3964
4051
|
EditorStore.setStatus('Delete failed: ' + message, 'error');
|
|
3965
4052
|
}
|
|
3966
4053
|
return refreshProjectTree().then(function () {
|
|
3967
|
-
GitService.
|
|
4054
|
+
GitService.fetchStatusLite({ background: true });
|
|
3968
4055
|
});
|
|
3969
4056
|
})["finally"](function () {
|
|
3970
4057
|
setLoading(function (prev) {
|
|
@@ -4024,7 +4111,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4024
4111
|
onCloseOthers: function (id) { handleCloseOtherTabs(paneId, id); },
|
|
4025
4112
|
onCloseSaved: function () { handleCloseSavedTabs(paneId); },
|
|
4026
4113
|
onCloseAll: function () { handleCloseEditorsInGroup(paneId); },
|
|
4027
|
-
onNewFile: function () {
|
|
4114
|
+
onNewFile: function () { TabManager.openUntitledTab(paneId); }
|
|
4028
4115
|
});
|
|
4029
4116
|
};
|
|
4030
4117
|
|
|
@@ -4877,7 +4964,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4877
4964
|
{
|
|
4878
4965
|
key: i,
|
|
4879
4966
|
className: "search-result-item",
|
|
4880
|
-
|
|
4967
|
+
// end_col puts the cursor just past the match, which is
|
|
4968
|
+
// where you want to start typing after jumping to a hit.
|
|
4969
|
+
onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line, r.end_col || r.col); }; })(res)
|
|
4881
4970
|
},
|
|
4882
4971
|
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
4883
4972
|
React.createElement(
|
|
@@ -5485,13 +5574,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5485
5574
|
)
|
|
5486
5575
|
),
|
|
5487
5576
|
React.createElement(
|
|
5488
|
-
'label', { className: 'ide-settings-row ide-settings-row-check', title: '
|
|
5489
|
-
React.createElement('span', { className: 'ide-settings-label' }, '
|
|
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'),
|
|
5490
5579
|
React.createElement('input', {
|
|
5491
5580
|
type: 'checkbox',
|
|
5492
5581
|
className: 'ide-settings-checkbox',
|
|
5493
|
-
checked: editorPrefs.
|
|
5494
|
-
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, {
|
|
5582
|
+
checked: editorPrefs.indentOnPaste !== false,
|
|
5583
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { indentOnPaste: v }); }); }
|
|
5495
5584
|
})
|
|
5496
5585
|
),
|
|
5497
5586
|
React.createElement(
|