mbeditor 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -1
  3. data/app/assets/javascripts/mbeditor/application.js +0 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +22 -2
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +11 -87
  6. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +171 -205
  8. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +35 -7
  9. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +97 -75
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +7 -1
  11. data/app/assets/javascripts/mbeditor/file_service.js +8 -18
  12. data/app/assets/javascripts/mbeditor/search_service.js +20 -2
  13. data/app/assets/javascripts/mbeditor/tab_manager.js +7 -4
  14. data/app/assets/stylesheets/mbeditor/editor.css +138 -67
  15. data/app/channels/mbeditor/collaboration_channel.rb +8 -2
  16. data/app/controllers/mbeditor/editors_controller.rb +8 -26
  17. data/app/services/mbeditor/collaboration_doc_store.rb +42 -3
  18. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  19. data/app/services/mbeditor/js_globals_service.rb +12 -1
  20. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  21. data/app/services/mbeditor/schema_service.rb +8 -2
  22. data/app/services/mbeditor/search_replace_service.rb +11 -2
  23. data/app/services/mbeditor/test_runner_service.rb +3 -56
  24. data/lib/mbeditor/configuration.rb +0 -8
  25. data/lib/mbeditor/route_map.rb +0 -1
  26. data/lib/mbeditor/version.rb +1 -1
  27. data/lib/tasks/mbeditor.rake +23 -0
  28. metadata +4 -3
  29. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b65424322b66f78929de0629db6e4644a76467e889adaee77121b8c52e0e50de
4
- data.tar.gz: 53cd7724f2a80011d474a9616bdb35d88859ddf797a0fa51d3657740038624bd
3
+ metadata.gz: fe5dce956e01ff66387b9ccf99fc8b163ce131a226158eda7b6cf2c8513eaa09
4
+ data.tar.gz: 37e4f12f4ccd1ca7d209e7fe134a25d813bbe175862d53a470a5042aff9d7797
5
5
  SHA512:
6
- metadata.gz: f4470580292e46d6de7eb2e3687193452bfbcbee5d478e48c6460e22739fd2942d1a525b04e6d3f7877175db5d18da86c3241032dfc748ca91bf62f0f008cd90
7
- data.tar.gz: 05e4226ce7151a7e72d18f34018c30acc04adfbaea235a891afe12ecadb52170b1b598820ea7de326068eae7bb7811cce13ec5cf42b0c69fd6c77683c3b7c819
6
+ metadata.gz: 60c74bb23b92719d4273d720af42813e2ecd3d639281901d47037e332697819cbe58c338eea9aeff334f3533d48d2e62c994b2122fb36fbded4bef94bd225b36
7
+ data.tar.gz: 1451f3b2effb5d9cffd9811b41ea72b07d174702af9e6dc9993d5fbc92d990109dbf834e957c1eced22e8d77b815173d14bf6465b51072002a0e1c482745dc69
data/CHANGELOG.md CHANGED
@@ -5,7 +5,72 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.13.1] - 2026-08-26
9
+
10
+ ### Removed
11
+ - **The whole-suite test run is gone** — the status-bar "Tests" button, the
12
+ Test Run drawer, `POST /test_all`, and the `test_all_command` /
13
+ `test_all_timeout` settings. It was one blocking request holding a
14
+ development-server thread for the length of the suite, with no way to cancel
15
+ it and the runner's output buffered whole in the server process; on a real
16
+ host app that is twenty minutes of a frozen-looking button for a result the
17
+ terminal gives you faster and interruptibly. The per-file (and per-line) test
18
+ run stays — that one is scoped, finishes in seconds and is why the editor
19
+ runs tests at all. **A host app setting `test_all_command` or
20
+ `test_all_timeout` in its initializer must drop those lines.**
21
+ - **The ruby-lsp status-bar chip is gone**, along with its 10-second health
22
+ poll. ruby-lsp itself is untouched — definitions, hover, completion,
23
+ diagnostics and the rest all still run through it.
24
+
25
+ ### Added
26
+ - `rake mbeditor:scan_duplicates` — scans the workspace for files that contain a
27
+ duplicated copy of themselves, for finding files damaged before the seeding fix below.
28
+ Set `MBEDITOR_WORKSPACE_ROOT` to scan a tree other than the resolved workspace.
29
+
30
+ ### Fixed
31
+ - **Data loss: a saved file could contain two copies of itself.** When two editor
32
+ clients attached to a collaborative room the server had no state for — a fresh
33
+ room, or one evicted while both sat idle — each one seeded the shared document
34
+ from disk, and Yjs merged both inserts at offset 0 into two concatenated copies.
35
+ Saving then wrote the doubled buffer. The server now grants the right to seed a
36
+ room to exactly one client (`CollaborationDocStore.claim_seed`), never evicts a
37
+ room that still has subscribers, and a client whose cable reconnects re-publishes
38
+ its snapshot so a server that lost its state cannot hand out a second seed.
39
+ - **Status bar alignment.** Every item now sits in the same 8px cell instead of
40
+ carrying its own padding on top of the bar's (the branch, the offline chip
41
+ and the version each had a different one), the branch drops from 13px to the
42
+ bar's 11px, and `line-height: 1` on the bar *and* on its buttons puts an icon
43
+ and the number beside it in identical line boxes — Pico gives `<button>` its
44
+ own 1.5, so the problem counts sat in a 16.5px box next to an 11px icon.
45
+ - **The Problems panel's Fix button did nothing visible.** `rubocop -a` writes
46
+ its corrections to disk, but reports the offenses it just fixed alongside the
47
+ ones it left — each flagged both `corrected` and `correctable` — so the list
48
+ after a fix was byte-identical to the list before it and the button stayed
49
+ lit. Corrected offenses are now dropped from the response, open buffers are
50
+ re-read after the run, and clearing stale diagnostics now clears the React
51
+ marker map as well as Monaco's, which is what had left the panel, the tab
52
+ badges and the status-bar tallies disagreeing.
53
+ - **Quick Open matches loosely again.** MiniSearch was configured `fuzzy: false`,
54
+ so a single typo found nothing; fuzziness is now scaled per term. Folders sort
55
+ after files instead of ahead of every match, and the dialog and its text are
56
+ larger.
57
+ - **Search results read like VS Code's.** Hits group under a collapsible row per
58
+ file with a match count, the matched text is tinted with the accent colour and
59
+ is kept on screen at any panel width (eliding left and right around it), and
60
+ clicking a hit selects the match and leaves the cursor at its end.
61
+ - **Bulk import no longer opens on the previous import's folder** — the folder
62
+ you actually acted on is offered first — and it now reports what it wrote and
63
+ reveals the destination in the tree.
64
+ - `ReactRailsUJS` no longer reports as undefined; it ships inside the react-rails
65
+ gem, outside the workspace, so no static scan of the tree could ever find it.
66
+ - `<<~JSX` heredocs highlight as JavaScript (as do `<<~TSX`/`<<~TS`).
67
+ - The Open Editors list no longer runs its filenames underneath the split and
68
+ close buttons.
69
+
70
+ ### Changed
71
+ - **The activity bar is a third narrower** (60px to 40px), and the editor
72
+ toolbar's per-file Test button is gone; running the test under the cursor is
73
+ unaffected.
9
74
 
10
75
  ## [0.13.0] - 2026-08-20
11
76
 
@@ -27,7 +27,6 @@
27
27
  //= require mbeditor/components/TestResultsPanel
28
28
  //= require mbeditor/components/LogPanel
29
29
  //= require mbeditor/components/ProblemsPanel
30
- //= require mbeditor/components/TestRunPanel
31
30
  //= require mbeditor/components/CodeReviewPanel
32
31
  //= require mbeditor/components/EditorPanel
33
32
  //= require mbeditor/components/FileTree
@@ -225,6 +225,9 @@ var CollaborationService = (function () {
225
225
  binding: null,
226
226
  undoManager: null,
227
227
  lateJoin: false,
228
+ // Set from the sync handshake: the server grants the right to seed an empty
229
+ // room's shared doc from disk to exactly one client (see claim_seed).
230
+ seedGranted: false,
228
231
  onSeeded: null,
229
232
  attachRequested: false,
230
233
  degraded: false,
@@ -280,10 +283,14 @@ var CollaborationService = (function () {
280
283
  if (d) window.Y.applyUpdate(room.doc, _b64ToU8(d), REMOTE);
281
284
  });
282
285
  }
286
+ room.seedGranted = !!data.seed;
283
287
  room.synced = true;
284
288
  _maybeAttach(room);
285
289
  } else if (data.type === 'doc_update') {
286
290
  if (data.update) window.Y.applyUpdate(room.doc, _b64ToU8(data.update), REMOTE);
291
+ // A client that deferred attaching (empty room, seed granted to someone
292
+ // else) is waiting for exactly this: the seeder's content has landed.
293
+ _maybeAttach(room);
287
294
  } else if (data.type === 'awareness') {
288
295
  if (data.awareness && room.awareness && window.awarenessProtocol) {
289
296
  // REMOTE origin keeps the outbound handler from echoing peer state back.
@@ -312,7 +319,13 @@ var CollaborationService = (function () {
312
319
  room.subscription = WebSocketService.subscribeCollaboration(path, {
313
320
  // Awareness is never server-persisted, so peers lost our caret with the
314
321
  // connection and would not see it again until we happened to move.
315
- connected: function () { _sendLocalAwareness(room); },
322
+ // The server may have lost the room entirely (restart): re-publish our
323
+ // content so the next opener late-joins instead of being granted a seed
324
+ // that would merge a second copy into what we still hold.
325
+ connected: function () {
326
+ _sendLocalAwareness(room);
327
+ if (room.binding) pushSnapshot(path);
328
+ },
316
329
  received: function (data) { _onMessage(room, data); }
317
330
  });
318
331
  });
@@ -346,6 +359,11 @@ var CollaborationService = (function () {
346
359
 
347
360
  function _maybeAttach(room) {
348
361
  if (room.binding || !room.synced || !room.attachRequested || !room.model) return;
362
+ // Shared doc still empty and the seed was granted to another client: its
363
+ // content is on its way. Binding now would hand the model to an empty Y.Text
364
+ // and wipe the buffer, so wait for the first doc_update (or the fallback
365
+ // timer, which lets the natively-loaded content stand).
366
+ if (room.text.length === 0 && !room.seedGranted && room.model.getValue().length > 0) return;
349
367
  _doAttach(room);
350
368
  }
351
369
 
@@ -353,10 +371,12 @@ var CollaborationService = (function () {
353
371
  var model = room.model;
354
372
  room.lateJoin = room.text.length > 0; // server already had shared state
355
373
 
356
- if (!room.lateJoin && model.getValue().length > 0) {
374
+ if (!room.lateJoin && room.seedGranted && model.getValue().length > 0) {
357
375
  // First opener whose disk content is already in the model: seed the shared
358
376
  // doc from it before constructing the binding, otherwise the binding would
359
377
  // overwrite the model to match the (empty) Y.Text and wipe the content.
378
+ // Only ever the client the SERVER picked — two clients each seeding an
379
+ // empty room merge into two concatenated copies of the file.
360
380
  room.doc.transact(function () { room.text.insert(0, model.getValue()); });
361
381
  }
362
382
 
@@ -24,16 +24,13 @@ var EditorPanel = function EditorPanel(_ref) {
24
24
  var onContentChange = _ref.onContentChange;
25
25
  var markers = _ref.markers;
26
26
  var gitAvailable = _ref.gitAvailable === true;
27
- var testAvailable = _ref.testAvailable === true;
28
27
  var onFormat = _ref.onFormat;
29
28
  var onSave = _ref.onSave;
30
- var onRunTest = _ref.onRunTest;
31
29
  var onRunTestAtCursor = _ref.onRunTestAtCursor;
32
30
  var onShowHistory = _ref.onShowHistory;
33
31
  var treeData = _ref.treeData || [];
34
32
  var testResult = _ref.testResult;
35
33
  var testPanelFile = _ref.testPanelFile;
36
- var testLoading = _ref.testLoading;
37
34
  var testInlineVisible = _ref.testInlineVisible;
38
35
  var editorPrefs = _ref.editorPrefs || {};
39
36
  var monacoReady = _ref.monacoReady !== false; // undefined means Monaco already loaded (legacy callers)
@@ -1323,9 +1320,18 @@ var EditorPanel = function EditorPanel(_ref) {
1323
1320
  var column = tab.gotoCol || 1;
1324
1321
  var maxColumn = model.getLineMaxColumn(line);
1325
1322
  if (column > maxColumn) column = maxColumn;
1323
+ // With both bounds the match itself is selected and the cursor ends up
1324
+ // just past it — where you'd start typing. With only gotoCol the
1325
+ // selection is empty, which is exactly setPosition.
1326
+ var endColumn = Math.min(tab.gotoEndCol || column, maxColumn);
1326
1327
 
1327
1328
  editor.revealLineInCenter(line);
1328
- editor.setPosition({ lineNumber: line, column: column });
1329
+ // setSelection(IRange) parks the cursor at the range's end, so no
1330
+ // setPosition afterwards — that would collapse the selection again.
1331
+ editor.setSelection({
1332
+ startLineNumber: line, startColumn: column,
1333
+ endLineNumber: line, endColumn: endColumn
1334
+ });
1329
1335
  editor.focus();
1330
1336
 
1331
1337
  TabManager.saveTabViewState(tab.id, editor.saveViewState());
@@ -1333,7 +1339,7 @@ var EditorPanel = function EditorPanel(_ref) {
1333
1339
  }, 50);
1334
1340
 
1335
1341
  return function () { clearTimeout(timer); };
1336
- }, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
1342
+ }, [tab.gotoLine, tab.gotoCol, tab.gotoEndCol, tab.content, tab.loading]);
1337
1343
 
1338
1344
  // Apply RuboCop markers
1339
1345
  useEffect(function () {
@@ -1803,74 +1809,6 @@ var EditorPanel = function EditorPanel(_ref) {
1803
1809
  return src === derived;
1804
1810
  };
1805
1811
 
1806
- var testFileCandidates = function(relativePath) {
1807
- if (!relativePath || !relativePath.endsWith('.rb')) return [];
1808
-
1809
- var basename = relativePath.slice(0, -3);
1810
- var dirParts = relativePath.split('/');
1811
- var leafName = basename.split('/').pop();
1812
- var candidates = [];
1813
-
1814
- if (dirParts[0] === 'app' && dirParts.length > 1) {
1815
- var subPath = dirParts.slice(1).join('/');
1816
- var subDir = subPath.indexOf('/') !== -1 ? subPath.slice(0, subPath.lastIndexOf('/')) : '';
1817
- candidates.push('test/' + (subDir ? subDir + '/' : '') + leafName + '_test.rb');
1818
- candidates.push('spec/' + (subDir ? subDir + '/' : '') + leafName + '_spec.rb');
1819
- }
1820
-
1821
- if (dirParts[0] === 'lib') {
1822
- var libSubPath = dirParts.slice(1).join('/');
1823
- var libSubDir = libSubPath.indexOf('/') !== -1 ? libSubPath.slice(0, libSubPath.lastIndexOf('/')) : '';
1824
- candidates.push('test/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
1825
- candidates.push('test/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
1826
- candidates.push('spec/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_spec.rb');
1827
- }
1828
-
1829
- candidates.push('test/' + leafName + '_test.rb');
1830
- candidates.push('spec/' + leafName + '_spec.rb');
1831
-
1832
- return candidates.filter(function(candidate, index, list) {
1833
- return list.indexOf(candidate) === index;
1834
- });
1835
- };
1836
-
1837
- var treeHasPath = function(nodes, targetPath) {
1838
- if (!targetPath) return false;
1839
- var stack = (nodes || []).slice();
1840
-
1841
- while (stack.length) {
1842
- var node = stack.pop();
1843
- if (!node) continue;
1844
- if (node.path === targetPath) return true;
1845
- if (node.children && node.children.length) {
1846
- stack.push.apply(stack, node.children);
1847
- }
1848
- }
1849
-
1850
- return false;
1851
- };
1852
-
1853
- var matchingTestFilePath = function(sourcePath) {
1854
- var normalized = (sourcePath || '').replace(/^\/+/, '');
1855
- if (!normalized || !normalized.endsWith('.rb')) return null;
1856
- if (/^(test|spec)\//.test(normalized) && /_(test|spec)\.rb$/.test(normalized)) return normalized;
1857
-
1858
- var candidates = testFileCandidates(normalized);
1859
- for (var i = 0; i < candidates.length; i += 1) {
1860
- if (treeHasPath(treeData, candidates[i])) return candidates[i];
1861
- }
1862
-
1863
- return null;
1864
- };
1865
-
1866
- // treeHasPath walks the whole file tree once per candidate, and the Test
1867
- // button asked for this on every render — so once per keystroke. treeData is
1868
- // deliberately outside the React.memo comparator below, so it can only change
1869
- // on a render this component was going to do anyway.
1870
- var testFileForTab = useMemo(function () {
1871
- return matchingTestFilePath(tab.path);
1872
- }, [tab.path, treeData]);
1873
-
1874
1812
  // Map a test method name to the best-matching line in the source file.
1875
1813
  // Extracts keywords from the test name and scores each source line.
1876
1814
  var mapTestToSourceLine = function(testName, sourceContent) {
@@ -2330,18 +2268,6 @@ var EditorPanel = function EditorPanel(_ref) {
2330
2268
  React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
2331
2269
  !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
2332
2270
  ),
2333
- testAvailable && testFileForTab && React.createElement(
2334
- 'button',
2335
- {
2336
- className: 'ide-icon-btn',
2337
- onClick: function() { if (onRunTest) onRunTest(); },
2338
- disabled: testLoading,
2339
- 'aria-busy': !!testLoading,
2340
- title: 'Run Tests'
2341
- },
2342
- !testLoading && React.createElement('i', { className: 'fas fa-flask', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
2343
- !editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
2344
- )
2345
2271
  ),
2346
2272
  conflictCount > 0 && React.createElement(
2347
2273
  'div', { className: 'mb-conflict-banner' },
@@ -2612,10 +2538,8 @@ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
2612
2538
  prev.paneId === next.paneId &&
2613
2539
  (prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
2614
2540
  prev.gitAvailable === next.gitAvailable &&
2615
- prev.testAvailable === next.testAvailable &&
2616
2541
  prev.testResult === next.testResult &&
2617
2542
  prev.testPanelFile === next.testPanelFile &&
2618
- prev.testLoading === next.testLoading &&
2619
2543
  prev.testInlineVisible === next.testInlineVisible &&
2620
2544
  prev.editorPrefs === next.editorPrefs &&
2621
2545
  prev.monacoReady === next.monacoReady;
@@ -88,7 +88,15 @@ var ImportDialog = function ImportDialog(_ref) {
88
88
  options.push(destOption(prefix, label, hint));
89
89
  };
90
90
 
91
- if (initialFolder) offer(initialFolder, initialFolder, 'selected folder');
91
+ // The folder this upload was started from is always first and always the
92
+ // active one. Offering it after the suggestions meant that once you had
93
+ // imported a folder anywhere, that same folder scored top of the suggestion
94
+ // list next time and sat at the head of the options — so the dialog read as
95
+ // "it remembered where I last put things". With nothing selected it was
96
+ // worse: the previous destination was the first option and the actually
97
+ // chosen one ("Workspace root") was last.
98
+ offer(initialFolder, initialFolder || 'Workspace root',
99
+ initialFolder ? 'selected folder' : 'create the folders from the root');
92
100
  suggestions.forEach(function (s) {
93
101
  var bits = [];
94
102
  if (s.files) bits.push(s.files + ' matching file' + (s.files === 1 ? '' : 's'));