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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f4e31cf2770935dab538488ed3b946b26c7bacfb8519a7d1069cd84d8064838
4
- data.tar.gz: db294fee8ac1938dc03f58d0332e13c2da4c8f2a322c62a22fbcd859b8d3b17c
3
+ metadata.gz: 264dfef604baf70be3fcb9984f588a93a8388fe13371cb3c8707d5ee7149d441
4
+ data.tar.gz: d584056fbb108ea55ddb98a819af73e4eabb939d659fc7d54442446b34b2c203
5
5
  SHA512:
6
- metadata.gz: d3d8769779acc2c71a535c2d324385f182937a117218471f42dccac8ebe10c0e0741b46da229d3f388c70d8f10c27e510ae25c0eb6e8507dc7967c6e694c46d6
7
- data.tar.gz: 130c85228b63ab45775d542086a7bb3104515cf07ecf15e88c8c23e8fe79f42207c9c1ab1f2f3ee16bf611a712c599335d5074e2060d1da90ac37dc52b833b09
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's own format-on-paste is left off: it never ran the formatter
651
- // here. attachEditorFeatures drives it from onDidPaste instead, gated on
652
- // the same editorPrefs.formatOnPaste setting.
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's own format-on-paste is left off: it never ran the formatter
1217
- // here. attachEditorFeatures drives it from onDidPaste instead, gated on
1218
- // the same editorPrefs.formatOnPaste setting.
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 && monacoRef.current) {
1307
- (function () {
1308
- var editor = monacoRef.current;
1309
- setTimeout(function () {
1310
- editor.revealLineInCenter(tab.gotoLine);
1311
- editor.setPosition({ lineNumber: tab.gotoLine, column: tab.gotoCol || 1 });
1312
- editor.focus();
1313
-
1314
- TabManager.saveTabViewState(tab.id, editor.saveViewState());
1315
- TabManager.clearGotoLine(paneId, tab.path);
1316
- }, 50);
1317
- })();
1318
- }
1319
- }, [tab.gotoLine, tab.content]); // need tab.content in dep array so if it loads asynchronously, the jump happens AFTER content loads
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
- formatOnPaste: true,
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
- }, 2000)).current;
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
- GitService.fetchStatus()["catch"](function () {});
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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.fetchStatus();
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
- onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line, r.col); }; })(res)
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: 'Auto-format pasted code using the language formatter' },
5554
- React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
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.formatOnPaste !== false,
5559
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
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 shown = needle
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
- 'span',
197
- { className: 'ide-problems-summary' },
198
- problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
199
- ', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
200
- ', ' + problems.infos.length + ' info' +
201
- ' in open files'
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
- // ── Format on paste ──────────────────────────────────────────────────────
819
+ // ── Indent on paste ──────────────────────────────────────────────────────
820
820
  //
821
- // Driven from onDidPaste rather than Monaco's own `formatOnPaste` option.
822
- // That option has been on by default all along and never did anything here:
823
- // its contribution is present and the event fires, but it declines to run
824
- // the formatter, so pasted code kept whatever indentation it was copied
825
- // with. onDidPaste is public API, fires reliably, and hands over the exact
826
- // pasted range — which is also what makes the two cases distinguishable:
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
- // * paste that fills the whole document (a blank file, or replacing all
829
- // of it) is formatted as a document
830
- // * a paste in the middle is formatted as a range, so Prettier reprints
831
- // the smallest enclosing statement and the rest of the file is left
832
- // byte-identical
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.formatOnPaste === false) return;
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 full = pasteModel.getFullModelRange();
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
- if (wholeDocument) {
851
- action.run()["catch"](function () { /* no formatter, or unparseable leave it */ });
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
- if (restore && restore.length && !editor.getModel().isDisposed()) editor.setSelections(restore);
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
- .ide-problems-summary { color: var(--ide-text-muted, #858585); font-size: 11px; }
3002
- .ide-problems-filter {
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: md.dig("lines", "text").to_s.strip
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
- { file: file_path, line: Regexp.last_match(2).to_i, text: Regexp.last_match(3).strip }
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.12.8"
4
+ VERSION = "0.12.9"
5
5
  end
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.8
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-07 00:00:00.000000000 Z
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails