mbeditor 0.12.8 → 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 +57 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +39 -21
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +40 -16
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +56 -7
- data/app/assets/javascripts/mbeditor/editor_plugins.js +41 -29
- data/app/assets/stylesheets/mbeditor/editor.css +34 -2
- data/app/services/mbeditor/search_replace_service.rb +44 -5
- 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,63 @@ 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
|
+
|
|
10
67
|
## [0.12.8] - 2026-08-07
|
|
11
68
|
|
|
12
69
|
### Added
|
|
@@ -647,9 +647,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
647
647
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
648
648
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
649
649
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
650
|
-
// Monaco
|
|
651
|
-
// here. attachEditorFeatures drives
|
|
652
|
-
//
|
|
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.
|
|
653
653
|
formatOnPaste: false,
|
|
654
654
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
655
655
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -1213,9 +1213,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1213
1213
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
1214
1214
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
1215
1215
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
1216
|
-
// Monaco
|
|
1217
|
-
// here. attachEditorFeatures drives
|
|
1218
|
-
//
|
|
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.
|
|
1219
1219
|
formatOnPaste: false,
|
|
1220
1220
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1221
1221
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -1301,22 +1301,40 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1301
1301
|
return function() { window.removeEventListener('mbeditor:focusPane', onFocusPane); };
|
|
1302
1302
|
}, [paneId]);
|
|
1303
1303
|
|
|
1304
|
-
// 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".
|
|
1305
1312
|
useEffect(function () {
|
|
1306
|
-
if (tab.gotoLine
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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]);
|
|
1320
1338
|
|
|
1321
1339
|
// Apply RuboCop markers
|
|
1322
1340
|
useEffect(function () {
|
|
@@ -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,
|
|
@@ -856,10 +856,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
856
856
|
});
|
|
857
857
|
};
|
|
858
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.
|
|
859
866
|
var refreshProjectTree = function refreshProjectTree() {
|
|
860
867
|
return FileService.getTree().then(function (data) {
|
|
861
868
|
setTreeData(data || []);
|
|
862
869
|
SearchService.buildIndex(data || []);
|
|
870
|
+
SearchService.invalidate();
|
|
871
|
+
if (searchQueryRef.current && searchPanelVisibleRef.current) {
|
|
872
|
+
_debouncedSearch(searchQueryRef.current);
|
|
873
|
+
}
|
|
863
874
|
return data || [];
|
|
864
875
|
})["catch"](function (err) {
|
|
865
876
|
EditorStore.setStatus("Failed to refresh files: " + (err && err.message || "Unknown error"), "error");
|
|
@@ -1663,14 +1674,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1663
1674
|
});
|
|
1664
1675
|
});
|
|
1665
1676
|
}, Promise.resolve());
|
|
1666
|
-
|
|
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;
|
|
1667
1682
|
|
|
1668
1683
|
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1669
1684
|
// and git status immediately (same work as the 10s poll below does).
|
|
1670
1685
|
useEffect(function () {
|
|
1671
1686
|
function handleFilesChanged(payload) {
|
|
1672
1687
|
if (document.hidden) return;
|
|
1673
|
-
|
|
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 () {});
|
|
1674
1696
|
FileService.getTree().then(function (data) {
|
|
1675
1697
|
setTreeData(_treeUpdater(data || []));
|
|
1676
1698
|
checkOpenTabsForExternalChanges(payload && payload.paths);
|
|
@@ -2004,7 +2026,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2004
2026
|
noteLocalSave(tab.path, tab.content);
|
|
2005
2027
|
EditorStore.setStatus("Saved", "success");
|
|
2006
2028
|
SearchService.invalidate();
|
|
2007
|
-
GitService.
|
|
2029
|
+
GitService.fetchStatusLite({ background: true });
|
|
2008
2030
|
// Reset the AVI clean baseline so undo past this save point shows dirty correctly.
|
|
2009
2031
|
var _closeEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
2010
2032
|
if (_closeEntry && _closeEntry.model && !_closeEntry.model.isDisposed()) {
|
|
@@ -2109,7 +2131,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2109
2131
|
return FileService.saveFile(newPath, tab.content).then(function () {
|
|
2110
2132
|
noteLocalSave(newPath, tab.content);
|
|
2111
2133
|
SearchService.invalidate();
|
|
2112
|
-
GitService.
|
|
2134
|
+
GitService.fetchStatusLite({ background: true });
|
|
2113
2135
|
FileService.getTree().then(function (data) { setTreeData(_treeUpdater(data || [])); })["catch"](function () {});
|
|
2114
2136
|
TabManager.closeTab(paneId, tab.id);
|
|
2115
2137
|
if (!(opts && opts.close)) {
|
|
@@ -2836,7 +2858,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2836
2858
|
})["catch"](function () {});
|
|
2837
2859
|
}
|
|
2838
2860
|
|
|
2839
|
-
GitService.
|
|
2861
|
+
GitService.fetchStatusLite({ background: true });
|
|
2840
2862
|
})["catch"](function (err) {
|
|
2841
2863
|
EditorStore.setStatus("Save failed: " + err.message, "error");
|
|
2842
2864
|
})["finally"](function () {
|
|
@@ -2947,7 +2969,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2947
2969
|
});
|
|
2948
2970
|
EditorStore.setStatus("All files saved", "success");
|
|
2949
2971
|
SearchService.invalidate();
|
|
2950
|
-
GitService.
|
|
2972
|
+
GitService.fetchStatusLite({ background: true });
|
|
2951
2973
|
})["catch"](function (err) {
|
|
2952
2974
|
EditorStore.setStatus("Failed to save some files", "error");
|
|
2953
2975
|
})["finally"](function () {
|
|
@@ -3872,7 +3894,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3872
3894
|
EditorStore.setStatus('Created file: ' + createdName, 'success');
|
|
3873
3895
|
return refreshProjectTree().then(function () {
|
|
3874
3896
|
handleSelectFile(createdPath, createdName);
|
|
3875
|
-
GitService.
|
|
3897
|
+
GitService.fetchStatusLite({ background: true });
|
|
3876
3898
|
});
|
|
3877
3899
|
})["catch"](function (err) {
|
|
3878
3900
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3893,7 +3915,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3893
3915
|
handleNodeSelect({ path: createdPath, name: createdPath.split('/').pop(), type: 'folder' });
|
|
3894
3916
|
EditorStore.setStatus('Created folder: ' + createdPath, 'success');
|
|
3895
3917
|
return refreshProjectTree().then(function () {
|
|
3896
|
-
return GitService.
|
|
3918
|
+
return GitService.fetchStatusLite({ background: true });
|
|
3897
3919
|
});
|
|
3898
3920
|
})["catch"](function (err) {
|
|
3899
3921
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -3968,7 +3990,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3968
3990
|
setSelectedPaths(new Set([renamedPath]));
|
|
3969
3991
|
EditorStore.setStatus('Renamed to: ' + renamedPath, 'success');
|
|
3970
3992
|
return refreshProjectTree().then(function () {
|
|
3971
|
-
GitService.
|
|
3993
|
+
GitService.fetchStatusLite({ background: true });
|
|
3972
3994
|
});
|
|
3973
3995
|
})["catch"](function (err) {
|
|
3974
3996
|
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
@@ -4029,7 +4051,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4029
4051
|
EditorStore.setStatus('Delete failed: ' + message, 'error');
|
|
4030
4052
|
}
|
|
4031
4053
|
return refreshProjectTree().then(function () {
|
|
4032
|
-
GitService.
|
|
4054
|
+
GitService.fetchStatusLite({ background: true });
|
|
4033
4055
|
});
|
|
4034
4056
|
})["finally"](function () {
|
|
4035
4057
|
setLoading(function (prev) {
|
|
@@ -4942,7 +4964,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4942
4964
|
{
|
|
4943
4965
|
key: i,
|
|
4944
4966
|
className: "search-result-item",
|
|
4945
|
-
|
|
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)
|
|
4946
4970
|
},
|
|
4947
4971
|
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
4948
4972
|
React.createElement(
|
|
@@ -5550,13 +5574,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5550
5574
|
)
|
|
5551
5575
|
),
|
|
5552
5576
|
React.createElement(
|
|
5553
|
-
'label', { className: 'ide-settings-row ide-settings-row-check', title: '
|
|
5554
|
-
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'),
|
|
5555
5579
|
React.createElement('input', {
|
|
5556
5580
|
type: 'checkbox',
|
|
5557
5581
|
className: 'ide-settings-checkbox',
|
|
5558
|
-
checked: editorPrefs.
|
|
5559
|
-
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 }); }); }
|
|
5560
5584
|
})
|
|
5561
5585
|
),
|
|
5562
5586
|
React.createElement(
|
|
@@ -96,6 +96,34 @@ var ProblemsPanel = (function () {
|
|
|
96
96
|
var _filter = React.useState('');
|
|
97
97
|
var filter = _filter[0], setFilter = _filter[1];
|
|
98
98
|
|
|
99
|
+
// Which severities the list shows. Multi-select rather than a single
|
|
100
|
+
// dropdown: "errors and warnings, but not the convention noise" is the
|
|
101
|
+
// view people actually want, and a one-of-three picker can't express it.
|
|
102
|
+
// Persisted, because a filter you have to re-set every time you open the
|
|
103
|
+
// panel is one you stop using.
|
|
104
|
+
var _severities = React.useState(function () {
|
|
105
|
+
try {
|
|
106
|
+
var saved = JSON.parse(window.localStorage.getItem('mbeditorProblemsSeverities'));
|
|
107
|
+
if (saved && typeof saved === 'object') {
|
|
108
|
+
return { error: saved.error !== false, warning: saved.warning !== false, info: saved.info !== false };
|
|
109
|
+
}
|
|
110
|
+
} catch (e) { /* unparseable or storage blocked — fall through to the default */ }
|
|
111
|
+
return { error: true, warning: true, info: true };
|
|
112
|
+
});
|
|
113
|
+
var severities = _severities[0], setSeverities = _severities[1];
|
|
114
|
+
|
|
115
|
+
var toggleSeverity = function (kind) {
|
|
116
|
+
setSeverities(function (prev) {
|
|
117
|
+
var next = Object.assign({}, prev);
|
|
118
|
+
next[kind] = !next[kind];
|
|
119
|
+
// Turning the last one off would show an empty panel with no hint as
|
|
120
|
+
// to why, so the final active severity stays latched on.
|
|
121
|
+
if (!next.error && !next.warning && !next.info) return prev;
|
|
122
|
+
try { window.localStorage.setItem('mbeditorProblemsSeverities', JSON.stringify(next)); } catch (e) {}
|
|
123
|
+
return next;
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
99
127
|
// Exceptions raised by the host app. Unlike markers these are not per-model
|
|
100
128
|
// — a runtime failure isn't a property of a file you happen to have open —
|
|
101
129
|
// so they live in their own section above the marker list.
|
|
@@ -165,11 +193,14 @@ var ProblemsPanel = (function () {
|
|
|
165
193
|
}, []);
|
|
166
194
|
|
|
167
195
|
var needle = filter.trim().toLowerCase();
|
|
168
|
-
var
|
|
196
|
+
var allSeverities = severities.error && severities.warning && severities.info;
|
|
197
|
+
var shown = (needle || !allSeverities)
|
|
169
198
|
? problems.byFile.map(function (entry) {
|
|
170
199
|
return {
|
|
171
200
|
path: entry.path,
|
|
172
201
|
markers: entry.markers.filter(function (item) {
|
|
202
|
+
if (!severities[SEVERITY_KIND[item.marker.severity] || 'info']) return false;
|
|
203
|
+
if (!needle) return true;
|
|
173
204
|
return (item.marker.message + ' ' + item.code + ' ' + entry.path)
|
|
174
205
|
.toLowerCase().indexOf(needle) !== -1;
|
|
175
206
|
})
|
|
@@ -192,13 +223,31 @@ var ProblemsPanel = (function () {
|
|
|
192
223
|
{ className: 'ide-problems-header' },
|
|
193
224
|
React.createElement('i', { className: 'fas fa-bug' }),
|
|
194
225
|
React.createElement('span', { className: 'ide-problems-title' }, 'Problems'),
|
|
226
|
+
// No prose summary: the severity chips below carry the same three
|
|
227
|
+
// counts, and restating them was the longest thing in the header.
|
|
195
228
|
React.createElement(
|
|
196
|
-
'
|
|
197
|
-
{ className: 'ide-problems-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
',
|
|
201
|
-
'
|
|
229
|
+
'div',
|
|
230
|
+
{ className: 'ide-problems-severity-filter' },
|
|
231
|
+
[
|
|
232
|
+
{ kind: 'error', count: problems.errors.length },
|
|
233
|
+
{ kind: 'warning', count: problems.warnings.length },
|
|
234
|
+
{ kind: 'info', count: problems.infos.length }
|
|
235
|
+
].map(function (s) {
|
|
236
|
+
var on = severities[s.kind];
|
|
237
|
+
return React.createElement(
|
|
238
|
+
'button',
|
|
239
|
+
{
|
|
240
|
+
key: s.kind,
|
|
241
|
+
type: 'button',
|
|
242
|
+
className: 'ide-problems-sev-chip ide-problems-sev-' + s.kind + (on ? ' is-on' : ''),
|
|
243
|
+
title: (on ? 'Hide' : 'Show') + ' ' + SEVERITY_LABEL[s.kind].toLowerCase() + 's',
|
|
244
|
+
'aria-pressed': on ? 'true' : 'false',
|
|
245
|
+
onClick: function () { toggleSeverity(s.kind); }
|
|
246
|
+
},
|
|
247
|
+
React.createElement('i', { className: 'fas ' + SEVERITY_ICON[s.kind] }),
|
|
248
|
+
React.createElement('span', null, s.count)
|
|
249
|
+
);
|
|
250
|
+
})
|
|
202
251
|
),
|
|
203
252
|
React.createElement('input', {
|
|
204
253
|
className: 'ide-problems-filter',
|
|
@@ -816,48 +816,37 @@
|
|
|
816
816
|
return handled;
|
|
817
817
|
});
|
|
818
818
|
|
|
819
|
-
// ──
|
|
819
|
+
// ── Indent on paste ──────────────────────────────────────────────────────
|
|
820
820
|
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
// the
|
|
825
|
-
//
|
|
826
|
-
//
|
|
821
|
+
// Pasted code is re-indented to match where it landed, and nothing else is
|
|
822
|
+
// touched. This deliberately does NOT run the formatter: routing paste
|
|
823
|
+
// through Prettier reprinted the whole enclosing statement (and, for a
|
|
824
|
+
// paste that filled the document, every line of it), so pasting a snippet
|
|
825
|
+
// rewrote code the user never touched and buried the paste in an unrelated
|
|
826
|
+
// diff.
|
|
827
827
|
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
// Either way the result comes back at the editor's own tab/space setting,
|
|
835
|
-
// which is the point: code copied in from a spaces project lands as tabs.
|
|
828
|
+
// `editor.action.reindentselectedlines` is Monaco's own re-indenter — it
|
|
829
|
+
// uses the language's indentation rules, the same ones that already run
|
|
830
|
+
// when you press Enter, and it only ever changes leading whitespace.
|
|
831
|
+
// Monaco's `formatOnPaste` option stays off (EditorPanel passes false):
|
|
832
|
+
// its contribution declines to run here anyway.
|
|
836
833
|
var pasteDisposable = editor.onDidPaste(function (e) {
|
|
837
834
|
var prefs = (typeof EditorStore !== 'undefined' && EditorStore.getState().editorPrefs) || {};
|
|
838
|
-
if (prefs.
|
|
835
|
+
if (prefs.indentOnPaste === false) return;
|
|
839
836
|
|
|
840
837
|
var pasteModel = editor.getModel();
|
|
841
838
|
if (!pasteModel || pasteModel.isDisposed()) return;
|
|
842
839
|
|
|
843
|
-
var
|
|
844
|
-
var wholeDocument = e.range.startLineNumber <= full.startLineNumber &&
|
|
845
|
-
e.range.endLineNumber >= full.endLineNumber;
|
|
846
|
-
|
|
847
|
-
var action = editor.getAction(wholeDocument ? 'editor.action.formatDocument' : 'editor.action.formatSelection');
|
|
840
|
+
var action = editor.getAction('editor.action.reindentselectedlines');
|
|
848
841
|
if (!action) return;
|
|
849
842
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// formatSelection works on the selection, so point it at the pasted range
|
|
856
|
-
// and put the cursor back where the paste left it.
|
|
843
|
+
// The action works on the selection, so point it at the pasted range and
|
|
844
|
+
// put the cursor back where the paste left it.
|
|
857
845
|
var restore = editor.getSelections();
|
|
858
846
|
editor.setSelection(e.range);
|
|
859
847
|
action.run()["catch"](function () {})["finally"](function () {
|
|
860
|
-
|
|
848
|
+
var m = editor.getModel();
|
|
849
|
+
if (restore && restore.length && m && !m.isDisposed()) editor.setSelections(restore);
|
|
861
850
|
});
|
|
862
851
|
});
|
|
863
852
|
|
|
@@ -1188,6 +1177,20 @@
|
|
|
1188
1177
|
});
|
|
1189
1178
|
}
|
|
1190
1179
|
|
|
1180
|
+
// Monaco ships no indentationRules for JavaScript, which makes
|
|
1181
|
+
// `editor.action.reindentselectedlines` — what indent-on-paste runs — a
|
|
1182
|
+
// silent no-op in JS/JSX files. These are VS Code's own JS rules.
|
|
1183
|
+
// setLanguageConfiguration merges, so this adds indentation without
|
|
1184
|
+
// disturbing the brackets/comments config the bundle already registers.
|
|
1185
|
+
['javascript', 'typescript'].forEach(function (langId) {
|
|
1186
|
+
monaco.languages.setLanguageConfiguration(langId, {
|
|
1187
|
+
indentationRules: {
|
|
1188
|
+
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/,
|
|
1189
|
+
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[})\]].*$/
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1191
1194
|
monaco.languages.setLanguageConfiguration('ruby', {
|
|
1192
1195
|
comments: { lineComment: '#', blockComment: ['=begin', '=end'] },
|
|
1193
1196
|
brackets: [['(', ')'], ['{', '}'], ['[', ']']],
|
|
@@ -1616,6 +1619,15 @@
|
|
|
1616
1619
|
// their "open this" through here.
|
|
1617
1620
|
monaco.editor.registerEditorOpener({
|
|
1618
1621
|
openCodeEditor: function (_source, resource, selectionOrPosition) {
|
|
1622
|
+
// Only file:// resources name a workspace file. Models are created
|
|
1623
|
+
// without an explicit URI, so Monaco gives each one an
|
|
1624
|
+
// `inmemory://model/N` identity — and the TS worker returns exactly
|
|
1625
|
+
// that when a JS definition resolves inside the file you are already
|
|
1626
|
+
// in. Stripping it to a path opened a phantom tab called "57".
|
|
1627
|
+
// Handing those back to Monaco lets it reveal the position in the
|
|
1628
|
+
// current editor, which is what the gesture meant.
|
|
1629
|
+
if (String(resource.scheme || '') !== 'file') return false;
|
|
1630
|
+
|
|
1619
1631
|
var path = String(resource.path || '').replace(/^\/+/, '');
|
|
1620
1632
|
if (!path || typeof TabManager === 'undefined' || !TabManager.openTab) return false;
|
|
1621
1633
|
|
|
@@ -2998,9 +2998,41 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
2998
2998
|
color: var(--ide-fg-muted, #ccc);
|
|
2999
2999
|
}
|
|
3000
3000
|
.ide-problems-title { font-weight: 600; }
|
|
3001
|
-
.
|
|
3002
|
-
.
|
|
3001
|
+
/* Severity toggles. Pushed to the right of the summary; the text filter that
|
|
3002
|
+
follows keeps its own left margin off `auto` so the two sit together. */
|
|
3003
|
+
.ide-problems-severity-filter {
|
|
3003
3004
|
margin-left: auto;
|
|
3005
|
+
display: flex;
|
|
3006
|
+
gap: 4px;
|
|
3007
|
+
}
|
|
3008
|
+
.ide-problems-sev-chip {
|
|
3009
|
+
display: inline-flex;
|
|
3010
|
+
align-items: center;
|
|
3011
|
+
gap: 4px;
|
|
3012
|
+
background: transparent;
|
|
3013
|
+
border: 1px solid transparent;
|
|
3014
|
+
border-radius: 3px;
|
|
3015
|
+
padding: 1px 6px;
|
|
3016
|
+
font-size: 11px;
|
|
3017
|
+
font-variant-numeric: tabular-nums;
|
|
3018
|
+
cursor: pointer;
|
|
3019
|
+
/* Off is legible but clearly recessive — a disabled-looking chip reads as
|
|
3020
|
+
"broken" rather than "toggled off". */
|
|
3021
|
+
color: var(--ide-text-muted, #858585);
|
|
3022
|
+
opacity: 0.55;
|
|
3023
|
+
}
|
|
3024
|
+
.ide-problems-sev-chip:hover { opacity: 0.85; }
|
|
3025
|
+
.ide-problems-sev-chip.is-on {
|
|
3026
|
+
opacity: 1;
|
|
3027
|
+
border-color: var(--ide-border, #333);
|
|
3028
|
+
background: var(--ide-input-bg, #2d2d2d);
|
|
3029
|
+
}
|
|
3030
|
+
.ide-problems-sev-chip.is-on.ide-problems-sev-error { color: var(--ide-error, #f14c4c); }
|
|
3031
|
+
.ide-problems-sev-chip.is-on.ide-problems-sev-warning { color: var(--ide-warning, #cca700); }
|
|
3032
|
+
.ide-problems-sev-chip.is-on.ide-problems-sev-info { color: var(--ide-info, #3794ff); }
|
|
3033
|
+
|
|
3034
|
+
.ide-problems-filter {
|
|
3035
|
+
margin-left: 8px;
|
|
3004
3036
|
background: var(--ide-input-bg, #2d2d2d);
|
|
3005
3037
|
color: var(--ide-fg, #eee);
|
|
3006
3038
|
border: 1px solid var(--ide-border, #333);
|
|
@@ -229,11 +229,20 @@ module Mbeditor
|
|
|
229
229
|
end
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
+
# Used to locate the match within each hit line so a result can carry
|
|
233
|
+
# its columns. Invalid user regexes are already reported elsewhere;
|
|
234
|
+
# here a nil pattern just means the rows come back without columns.
|
|
235
|
+
pattern = begin
|
|
236
|
+
build_pattern(query, use_regex: use_regex, match_case: match_case, whole_word: whole_word)
|
|
237
|
+
rescue RegexpError
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
232
241
|
begin
|
|
233
242
|
io.each_line do |raw|
|
|
234
243
|
break if results.length >= max
|
|
235
244
|
|
|
236
|
-
row = parse_line(tier, raw, root)
|
|
245
|
+
row = parse_line(tier, raw, root, pattern)
|
|
237
246
|
next unless row
|
|
238
247
|
next if matcher.excluded?(row[:file])
|
|
239
248
|
|
|
@@ -336,7 +345,7 @@ module Mbeditor
|
|
|
336
345
|
end
|
|
337
346
|
end
|
|
338
347
|
|
|
339
|
-
def parse_line(tier, raw, root)
|
|
348
|
+
def parse_line(tier, raw, root, pattern = nil)
|
|
340
349
|
if tier == :rg
|
|
341
350
|
begin
|
|
342
351
|
data = JSON.parse(raw)
|
|
@@ -346,11 +355,23 @@ module Mbeditor
|
|
|
346
355
|
return nil unless data["type"] == "match"
|
|
347
356
|
|
|
348
357
|
md = data["data"]
|
|
358
|
+
raw_text = md.dig("lines", "text").to_s
|
|
359
|
+
# rg reports submatch offsets in BYTES; Monaco columns are character
|
|
360
|
+
# based, so slice the prefix and measure it as characters.
|
|
361
|
+
sub = Array(md["submatches"]).first
|
|
362
|
+
cols = if sub && sub["start"] && sub["end"]
|
|
363
|
+
bytes = raw_text.dup.force_encoding(Encoding::BINARY)
|
|
364
|
+
start_chars = bytes[0, sub["start"]].to_s.force_encoding(Encoding::UTF_8).scrub.length
|
|
365
|
+
match_chars = bytes[sub["start"], sub["end"] - sub["start"]].to_s.force_encoding(Encoding::UTF_8).scrub.length
|
|
366
|
+
{ col: start_chars + 1, end_col: start_chars + match_chars + 1 }
|
|
367
|
+
else
|
|
368
|
+
match_columns(raw_text, pattern)
|
|
369
|
+
end
|
|
349
370
|
return {
|
|
350
371
|
file: relative_path(md.dig("path", "text").to_s, root),
|
|
351
372
|
line: md.dig("line_number"),
|
|
352
|
-
text:
|
|
353
|
-
}
|
|
373
|
+
text: raw_text.strip
|
|
374
|
+
}.merge(cols)
|
|
354
375
|
end
|
|
355
376
|
|
|
356
377
|
# git grep / grep emit "path:line:text" — possibly with bytes that are
|
|
@@ -367,7 +388,25 @@ module Mbeditor
|
|
|
367
388
|
file_path = relative_path(file_path, root)
|
|
368
389
|
end
|
|
369
390
|
|
|
370
|
-
|
|
391
|
+
raw_text = Regexp.last_match(3)
|
|
392
|
+
{ file: file_path, line: Regexp.last_match(2).to_i, text: raw_text.strip }
|
|
393
|
+
.merge(match_columns(raw_text, pattern))
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# 1-based Monaco columns for the first match on a hit line. Returns an
|
|
397
|
+
# empty hash when there is no usable pattern or it doesn't match — the
|
|
398
|
+
# row is still a valid result, it just opens at the start of the line.
|
|
399
|
+
# Measured against the RAW line, never the stripped `text`: the client
|
|
400
|
+
# cannot recover the leading whitespace the strip removed.
|
|
401
|
+
def match_columns(raw_text, pattern)
|
|
402
|
+
return {} unless pattern
|
|
403
|
+
|
|
404
|
+
m = pattern.match(raw_text)
|
|
405
|
+
return {} unless m
|
|
406
|
+
|
|
407
|
+
{ col: m.begin(0) + 1, end_col: m.end(0) + 1 }
|
|
408
|
+
rescue StandardError
|
|
409
|
+
{}
|
|
371
410
|
end
|
|
372
411
|
|
|
373
412
|
def register_search(root, pid)
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|