mbeditor 0.5.0 → 0.5.2

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: 79b3c0567fff4f540eeca0a01d4f5133192e55b200b920808295c443b67d5f6a
4
- data.tar.gz: 54f7fa3e433a9dbd34b17556fa0abfb244b220b4800af1997bea0be700955b22
3
+ metadata.gz: cd0837fbeccc3634d7804e6f9957bbbaca06bb4c02942eb4decc5e8b174499ab
4
+ data.tar.gz: 9f97239369b2968c17cd3909abdced4917329a349564d02ebde6beb89cb8c36f
5
5
  SHA512:
6
- metadata.gz: 1f2e2cec02b0019326c1e16e3659da31c9d6387fd84c8d38e0ca72be1ea007e22b4e5fc26512caa0a620f30ca589718e149bab210a1eeef17f349ba452289c1d
7
- data.tar.gz: bd145f73f0df001904fa139a8365f9dae815c9ad391bc8bc77aaefd0cb64a6317f8c74f0d43bdabcbab6c5878a6cba7b0155219dceb0b57d038814bd96096864
6
+ metadata.gz: 6310dc010236970bf25c0945a465122e1f8a081c399696d8870dc852d85454153863b01b1f1e9a8871abc3fbe07b1489724a065835c2909daa15426899701833
7
+ data.tar.gz: f8ed953601769d6785a4fecd4c3b16a6ce820e6e7f5c88a7cb12750d8f32b5122dc6bc25fd67be5e86bd7ef02ae1beba9ece2dbc5fc5bc0324df47785a03c9ba
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ 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
+ ## [0.5.1] - 2026-04-30
9
+
10
+ ### Added
11
+ - **Large file pagination** — files over 5 MB now open in read-only paginated mode (500 lines per page) instead of showing an error. A bar below the toolbar shows the current line range, total line count, and file size, with Prev/Next navigation. The backend streams only the requested line slice via `File.foreach` so arbitrarily large files never load fully into memory.
12
+ - **JSON auto pretty-print** — `.json` files are automatically formatted with 2-space indentation when opened. Invalid JSON falls back to raw display with Monaco's built-in error markers. The formatted content is set as the editor baseline so the file does not appear dirty after opening.
13
+
8
14
  ## [0.5.0] - 2026-04-30
9
15
 
10
16
  ### Added
@@ -86,6 +86,27 @@ var EditorPanel = function EditorPanel(_ref) {
86
86
 
87
87
  var methodsBtnRef = useRef(null);
88
88
 
89
+ // Local pagination state — initialized from tab props; updated on page navigation
90
+ var _useState17 = useState(tab.startLine || 0);
91
+ var _useState18 = _slicedToArray(_useState17, 2);
92
+ var pageStartLine = _useState18[0];
93
+ var setPageStartLine = _useState18[1];
94
+
95
+ var _useState19 = useState(tab.lineCount || 0);
96
+ var _useState20 = _slicedToArray(_useState19, 2);
97
+ var pageLineCount = _useState20[0];
98
+ var setPageLineCount = _useState20[1];
99
+
100
+ var _useState21 = useState(tab.totalLines || 0);
101
+ var _useState22 = _slicedToArray(_useState21, 2);
102
+ var pageTotalLines = _useState22[0];
103
+ var setPageTotalLines = _useState22[1];
104
+
105
+ var _useState23 = useState(tab.totalBytes || 0);
106
+ var _useState24 = _slicedToArray(_useState23, 2);
107
+ var pageTotalBytes = _useState24[0];
108
+ var setPageTotalBytes = _useState24[1];
109
+
89
110
  var onFormatRef = useRef(onFormat);
90
111
  onFormatRef.current = onFormat;
91
112
 
@@ -95,6 +116,12 @@ var EditorPanel = function EditorPanel(_ref) {
95
116
  var vimStatusRef = useRef(null);
96
117
  var vimModeObjRef = useRef(null);
97
118
 
119
+ function humanSize(bytes) {
120
+ if (bytes < 1024) return bytes + ' B';
121
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
122
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
123
+ }
124
+
98
125
  var clearTestZones = function clearTestZones(editor) {
99
126
  if (!editor) return;
100
127
  if (testZoneIdsRef.current.length === 0) return;
@@ -488,7 +515,18 @@ var EditorPanel = function EditorPanel(_ref) {
488
515
  } else {
489
516
  // Evict the LRU model if the cache is at capacity before creating a new one.
490
517
  TabManager.evictLruModel();
491
- modelObj = window.monaco.editor.createModel(tab.content, language);
518
+
519
+ // Pretty-print JSON content before initial load
520
+ var contentForModel = tab.content;
521
+ if (language === 'json' && contentForModel) {
522
+ try {
523
+ contentForModel = JSON.stringify(JSON.parse(contentForModel), null, 2);
524
+ } catch (_) {
525
+ // invalid JSON — use raw content; Monaco will show error markers
526
+ }
527
+ }
528
+
529
+ modelObj = window.monaco.editor.createModel(contentForModel, language);
492
530
  window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null, lastAccessed: Date.now(), cleanVersionId: null };
493
531
  _modelEntry = window.__mbeditorModels[tab.path];
494
532
  }
@@ -578,6 +616,10 @@ var EditorPanel = function EditorPanel(_ref) {
578
616
 
579
617
  monacoRef.current = editor;
580
618
  window.__mbeditorActiveEditor = editor;
619
+ // Apply read-only for paginated (truncated) files
620
+ if (tab.truncated) {
621
+ editor.updateOptions({ readOnly: true });
622
+ }
581
623
  setEditorReady(true);
582
624
 
583
625
  // Stash the workspace-relative path on the model so code-action providers
@@ -768,7 +810,19 @@ var EditorPanel = function EditorPanel(_ref) {
768
810
  // fires during setValue and skips the dirty check (cleanVersionId is null).
769
811
  var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
770
812
  if (_initEntry) _initEntry.cleanVersionId = null;
771
- editor.setValue(tab.content);
813
+
814
+ // Pretty-print JSON content before initial load
815
+ var contentToSet = tab.content;
816
+ var modelLang = model.getLanguageId();
817
+ if (modelLang === 'json' && contentToSet) {
818
+ try {
819
+ contentToSet = JSON.stringify(JSON.parse(contentToSet), null, 2);
820
+ } catch (_) {
821
+ // invalid JSON — use raw content; Monaco will show error markers
822
+ }
823
+ }
824
+
825
+ editor.setValue(contentToSet);
772
826
  // Reset the AVI baseline: setValue clears the undo stack so anything before
773
827
  // this point is no longer reachable. Also clear the canUndo/canRedo display.
774
828
  var newBase = model.getAlternativeVersionId();
@@ -917,6 +971,21 @@ var EditorPanel = function EditorPanel(_ref) {
917
971
  }
918
972
  }, [markers, tab.id]);
919
973
 
974
+ // Sync pagination state when tab changes (different file or fresh load)
975
+ useEffect(function () {
976
+ setPageStartLine(tab.startLine || 0);
977
+ setPageLineCount(tab.lineCount || 0);
978
+ setPageTotalLines(tab.totalLines || 0);
979
+ setPageTotalBytes(tab.totalBytes || 0);
980
+ }, [tab.id]);
981
+
982
+ // Apply read-only mode based on tab.truncated whenever the editor or truncated flag changes
983
+ useEffect(function () {
984
+ if (monacoRef.current) {
985
+ monacoRef.current.updateOptions({ readOnly: !!tab.truncated });
986
+ }
987
+ }, [tab.truncated, editorReady]);
988
+
920
989
  // Reset blame + test decorations when file path changes
921
990
  useEffect(function () {
922
991
  setBlameData(null);
@@ -1456,6 +1525,105 @@ var EditorPanel = function EditorPanel(_ref) {
1456
1525
  !editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
1457
1526
  )
1458
1527
  ),
1528
+ tab.truncated && React.createElement(
1529
+ 'div',
1530
+ {
1531
+ className: 'ide-pagination-bar',
1532
+ style: {
1533
+ display: 'flex',
1534
+ alignItems: 'center',
1535
+ gap: '8px',
1536
+ padding: '4px 10px',
1537
+ background: 'var(--ide-toolbar-bg, #252526)',
1538
+ borderBottom: '1px solid var(--ide-border, #3e3e3e)',
1539
+ fontSize: '12px',
1540
+ color: 'var(--ide-toolbar-fg, #ccc)',
1541
+ flexShrink: 0,
1542
+ userSelect: 'none'
1543
+ }
1544
+ },
1545
+ React.createElement(
1546
+ 'button',
1547
+ {
1548
+ className: 'ide-icon-btn',
1549
+ style: { padding: '2px 8px', fontSize: '12px' },
1550
+ disabled: pageStartLine === 0,
1551
+ onClick: function() {
1552
+ var newStart = Math.max(0, pageStartLine - 500);
1553
+ FileService.getFileChunk(tab.path, newStart, 500).then(function(data) {
1554
+ var sl = data.start_line || 0;
1555
+ var lc = data.line_count || 0;
1556
+ var tl = data.total_lines || pageTotalLines;
1557
+ var tb = data.total_bytes || pageTotalBytes;
1558
+ setPageStartLine(sl);
1559
+ setPageLineCount(lc);
1560
+ setPageTotalLines(tl);
1561
+ setPageTotalBytes(tb);
1562
+ if (monacoRef.current) {
1563
+ monacoRef.current.setValue(data.content || '');
1564
+ var _paginatedEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1565
+ if (_paginatedEntry) {
1566
+ var _newBase = monacoRef.current.getModel().getAlternativeVersionId();
1567
+ aviBaseRef.current = _newBase;
1568
+ aviMaxRef.current = _newBase;
1569
+ _paginatedEntry.cleanVersionId = _newBase;
1570
+ _paginatedEntry.aviBase = _newBase;
1571
+ _paginatedEntry.aviMax = _newBase;
1572
+ }
1573
+ EditorStore.setState({ canUndo: false, canRedo: false });
1574
+ monacoRef.current.updateOptions({ readOnly: true });
1575
+ }
1576
+ }).catch(function(err) {
1577
+ EditorStore.setStatus('Failed to load page: ' + (err && err.message || 'Unknown error'), 'error');
1578
+ });
1579
+ }
1580
+ },
1581
+ '← Prev'
1582
+ ),
1583
+ React.createElement(
1584
+ 'span',
1585
+ { style: { flex: 1, textAlign: 'center' } },
1586
+ 'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + humanSize(pageTotalBytes) + ')'
1587
+ ),
1588
+ React.createElement(
1589
+ 'button',
1590
+ {
1591
+ className: 'ide-icon-btn',
1592
+ style: { padding: '2px 8px', fontSize: '12px' },
1593
+ disabled: pageStartLine + pageLineCount >= pageTotalLines,
1594
+ onClick: function() {
1595
+ var newStart = pageStartLine + pageLineCount;
1596
+ FileService.getFileChunk(tab.path, newStart, 500).then(function(data) {
1597
+ var sl = data.start_line || 0;
1598
+ var lc = data.line_count || 0;
1599
+ var tl = data.total_lines || pageTotalLines;
1600
+ var tb = data.total_bytes || pageTotalBytes;
1601
+ setPageStartLine(sl);
1602
+ setPageLineCount(lc);
1603
+ setPageTotalLines(tl);
1604
+ setPageTotalBytes(tb);
1605
+ if (monacoRef.current) {
1606
+ monacoRef.current.setValue(data.content || '');
1607
+ var _paginatedEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1608
+ if (_paginatedEntry) {
1609
+ var _newBase = monacoRef.current.getModel().getAlternativeVersionId();
1610
+ aviBaseRef.current = _newBase;
1611
+ aviMaxRef.current = _newBase;
1612
+ _paginatedEntry.cleanVersionId = _newBase;
1613
+ _paginatedEntry.aviBase = _newBase;
1614
+ _paginatedEntry.aviMax = _newBase;
1615
+ }
1616
+ EditorStore.setState({ canUndo: false, canRedo: false });
1617
+ monacoRef.current.updateOptions({ readOnly: true });
1618
+ }
1619
+ }).catch(function(err) {
1620
+ EditorStore.setStatus('Failed to load page: ' + (err && err.message || 'Unknown error'), 'error');
1621
+ });
1622
+ }
1623
+ },
1624
+ 'Next →'
1625
+ )
1626
+ ),
1459
1627
  React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
1460
1628
  methodsOpen && methodsDropdownPos && React.createElement(
1461
1629
  'div',
@@ -12,6 +12,13 @@ var useRef = _React.useRef;
12
12
  var useEffect = _React.useEffect;
13
13
  var useMemo = _React.useMemo;
14
14
 
15
+ function formatSize(bytes) {
16
+ if (typeof bytes !== 'number' || bytes < 0) return '';
17
+ if (bytes < 1024) return bytes + ' B';
18
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
19
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
20
+ }
21
+
15
22
  var FileTree = function FileTree(_ref) {
16
23
  var items = _ref.items;
17
24
  var onSelect = _ref.onSelect;
@@ -477,7 +484,7 @@ var FileTree = function FileTree(_ref) {
477
484
  ),
478
485
  React.createElement(
479
486
  'div',
480
- { className: 'tree-item-name', title: node.path },
487
+ { className: 'tree-item-name', title: node.type === 'file' && node.size != null ? node.path + ' — ' + formatSize(node.size) : node.path },
481
488
  node.name
482
489
  ),
483
490
  statusMeta && React.createElement(
@@ -51,6 +51,35 @@
51
51
 
52
52
  var globalsRegistered = false;
53
53
 
54
+ // Enumerate window for user-defined globals and return a TypeScript declaration string.
55
+ // Sprockets exposes every top-level var/function as a window property before Monaco
56
+ // initialises, so scanning at registration time captures all components and helpers.
57
+ //
58
+ // Filter: keep only plain writable data properties (configurable, writable, no getter).
59
+ // Browser built-ins are either non-configurable or accessor properties (hasGet), so
60
+ // this reliably separates them from user-assigned globals without a native-code test
61
+ // (which only works for functions, not objects like `document` or `location`).
62
+ function buildWindowGlobalsShim() {
63
+ var alreadyDeclared = { React: 1, ReactDOM: 1, PropTypes: 1, MaterialUI: 1, $: 1, jQuery: 1 };
64
+ var lines = [];
65
+ try {
66
+ var keys = Object.keys(window);
67
+ for (var i = 0; i < keys.length; i++) {
68
+ var key = keys[i];
69
+ if (alreadyDeclared[key]) continue;
70
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) continue;
71
+ var desc;
72
+ try { desc = Object.getOwnPropertyDescriptor(window, key); } catch (e) { continue; }
73
+ if (!desc || !desc.configurable || !desc.writable || desc.get) continue;
74
+ var value;
75
+ try { value = window[key]; } catch (e) { continue; }
76
+ if (value === null || value === undefined) continue;
77
+ lines.push('declare var ' + key + ': any;');
78
+ }
79
+ } catch (e) {}
80
+ return lines.join('\n');
81
+ }
82
+
54
83
  function leadingWhitespace(line) {
55
84
  var match = line.match(/^\s*/);
56
85
  return match ? match[0] : '';
@@ -295,6 +324,30 @@
295
324
  event.stopPropagation();
296
325
  });
297
326
 
327
+ // Navigate to a Ruby symbol: modules/classes go to their definition file,
328
+ // lowercase symbols go to their def line.
329
+ function navigateToWord(word) {
330
+ if (/^[A-Z]/.test(word) && typeof FileService !== 'undefined' && FileService.getModuleMembers) {
331
+ FileService.getModuleMembers(word).then(function(data) {
332
+ if (!data || !data.file) return;
333
+ var filename = data.file.split('/').pop();
334
+ if (typeof TabManager !== 'undefined' && TabManager.openTab) {
335
+ TabManager.openTab(data.file, filename, 1);
336
+ }
337
+ }).catch(function() {});
338
+ return;
339
+ }
340
+ if (typeof FileService === 'undefined' || !FileService.getDefinition) return;
341
+ FileService.getDefinition(word, 'ruby').then(function(data) {
342
+ var results = data && Array.isArray(data.results) ? data.results : [];
343
+ if (results.length === 0) return;
344
+ var r = results[0];
345
+ if (typeof TabManager !== 'undefined' && TabManager.openTab) {
346
+ TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
347
+ }
348
+ }).catch(function() {});
349
+ }
350
+
298
351
  // Ctrl/Cmd+click — navigate to definition
299
352
  gotoMouseDisposable = editor.onMouseDown(function(event) {
300
353
  var ctrlOrCmd = event.event.ctrlKey || event.event.metaKey;
@@ -309,19 +362,9 @@
309
362
  if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
310
363
  if (RUBY_KEYWORDS[wordInfo.word]) return;
311
364
  if (RUBY_CORE_METHODS[wordInfo.word]) return;
312
- if (typeof FileService === 'undefined' || !FileService.getDefinition) return;
313
365
 
314
366
  event.event.preventDefault();
315
-
316
- FileService.getDefinition(wordInfo.word, 'ruby').then(function(data) {
317
- var results = data && Array.isArray(data.results) ? data.results : [];
318
- if (results.length === 0) return;
319
- var r = results[0];
320
- var filename = r.file.split('/').pop();
321
- if (typeof TabManager !== 'undefined' && TabManager.openTab) {
322
- TabManager.openTab(r.file, filename, r.line);
323
- }
324
- }).catch(function() {});
367
+ navigateToWord(wordInfo.word);
325
368
  });
326
369
 
327
370
  // F12 — go to definition from keyboard
@@ -338,17 +381,7 @@
338
381
  if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
339
382
  if (RUBY_KEYWORDS[wordInfo.word]) return;
340
383
  if (RUBY_CORE_METHODS[wordInfo.word]) return;
341
- if (typeof FileService === 'undefined' || !FileService.getDefinition) return;
342
-
343
- FileService.getDefinition(wordInfo.word, 'ruby').then(function(data) {
344
- var results = data && Array.isArray(data.results) ? data.results : [];
345
- if (results.length === 0) return;
346
- var r = results[0];
347
- var filename = r.file.split('/').pop();
348
- if (typeof TabManager !== 'undefined' && TabManager.openTab) {
349
- TabManager.openTab(r.file, filename, r.line);
350
- }
351
- }).catch(function() {});
384
+ navigateToWord(wordInfo.word);
352
385
  }
353
386
  });
354
387
  }
@@ -455,6 +488,66 @@
455
488
  });
456
489
  }
457
490
 
491
+ // Declare globals that the sprockets asset pipeline injects at runtime so
492
+ // checkJs doesn't flag them as undefined. `interface Window` augmentation
493
+ // covers `window.myAppGlobal` access patterns. For app-specific component
494
+ // names not listed here, add `/* global MyComponent */` at the top of the
495
+ // file — TypeScript's checkJs mode respects that directive.
496
+ if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
497
+ monaco.languages.typescript.javascriptDefaults.addExtraLib(
498
+ [
499
+ 'declare var React: any;',
500
+ 'declare var ReactDOM: any;',
501
+ 'declare var PropTypes: any;',
502
+ 'declare var MaterialUI: any;',
503
+ 'declare var $: any;',
504
+ 'declare var jQuery: any;',
505
+ 'interface Window { [key: string]: any; }'
506
+ ].join('\n'),
507
+ 'inmemory://mbeditor/sprockets-globals.d.ts'
508
+ );
509
+
510
+ var dynamicShim = buildWindowGlobalsShim();
511
+ if (dynamicShim) {
512
+ monaco.languages.typescript.javascriptDefaults.addExtraLib(
513
+ dynamicShim,
514
+ 'inmemory://mbeditor/window-globals.d.ts'
515
+ );
516
+ }
517
+
518
+ // Downgrade "declared but never read" (TS6133) from Error to Warning.
519
+ // TypeScript has no built-in way to emit this as a warning, so we intercept
520
+ // the marker set after the worker fires and re-apply with lower severity.
521
+ // JS files use owner 'javascript', TS files use 'typescript'.
522
+ var WARN_CODES = { '6133': true };
523
+ var TS_OWNERS = ['javascript', 'typescript'];
524
+ var _severityPatchActive = false;
525
+ monaco.editor.onDidChangeMarkers(function(uris) {
526
+ if (_severityPatchActive) return;
527
+ _severityPatchActive = true;
528
+ try {
529
+ uris.forEach(function(uri) {
530
+ var model = monaco.editor.getModel(uri);
531
+ if (!model) return;
532
+ TS_OWNERS.forEach(function(owner) {
533
+ var markers = monaco.editor.getModelMarkers({ resource: uri, owner: owner });
534
+ var needsPatch = markers.some(function(m) {
535
+ return m.severity === monaco.MarkerSeverity.Error && WARN_CODES[String(m.code)];
536
+ });
537
+ if (!needsPatch) return;
538
+ monaco.editor.setModelMarkers(model, owner, markers.map(function(m) {
539
+ return (m.severity === monaco.MarkerSeverity.Error && WARN_CODES[String(m.code)])
540
+ ? Object.assign({}, m, { severity: monaco.MarkerSeverity.Warning })
541
+ : m;
542
+ }));
543
+ });
544
+ });
545
+ } finally {
546
+ _severityPatchActive = false;
547
+ }
548
+ });
549
+ }
550
+
458
551
  // TypeScript: enable JSX for .tsx files and catch unused locals.
459
552
  if (monaco.languages.typescript && monaco.languages.typescript.typescriptDefaults) {
460
553
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
@@ -54,6 +54,14 @@ var FileService = (function () {
54
54
  return axios.get(window.mbeditorBasePath() + '/file', { params: params }).then(function(res) { return res.data; });
55
55
  }
56
56
 
57
+ function getFileChunk(path, startLine, lineCount) {
58
+ if (lineCount === undefined) {
59
+ lineCount = 500;
60
+ }
61
+ var params = { path: path, start_line: startLine, line_count: lineCount };
62
+ return axios.get(window.mbeditorBasePath() + '/file', { params: params }).then(function(res) { return res.data; });
63
+ }
64
+
57
65
  function saveFile(path, code) {
58
66
  return axios.post(window.mbeditorBasePath() + '/file', { path: path, code: code }).then(function(res) { return res.data; });
59
67
  }
@@ -200,6 +208,7 @@ var FileService = (function () {
200
208
  getWorkspace: getWorkspace,
201
209
  getTree: getTree,
202
210
  getFile: getFile,
211
+ getFileChunk: getFileChunk,
203
212
  saveFile: saveFile,
204
213
  createFile: createFile,
205
214
  createDir: createDir,
@@ -207,6 +207,29 @@ var TabManager = (function () {
207
207
  }
208
208
  }).catch(function(err) {
209
209
  if (path.startsWith('diff://')) return; // diff tabs handle their own loading
210
+ if (err.response && err.response.status === 413) {
211
+ FileService.getFileChunk(path, 0, 500).then(function(data) {
212
+ var loadedContent = typeof data.content === 'string' ? data.content : "";
213
+ _updateTab(paneId, path, {
214
+ content: loadedContent,
215
+ cleanContent: loadedContent,
216
+ externalContentVersion: 1,
217
+ isImage: false,
218
+ fileNotFound: false,
219
+ dirty: false,
220
+ loading: false,
221
+ truncated: true,
222
+ startLine: data.start_line || 0,
223
+ lineCount: data.line_count || 0,
224
+ totalLines: data.total_lines || 0,
225
+ totalBytes: data.total_bytes || 0
226
+ });
227
+ }).catch(function(chunkErr) {
228
+ EditorStore.setStatus("Failed to load large file: " + ((chunkErr.response && chunkErr.response.data && chunkErr.response.data.error) || chunkErr.message), "error");
229
+ closeTab(paneId, path);
230
+ });
231
+ return;
232
+ }
210
233
  EditorStore.setStatus("Failed to load file: " + ((err.response && err.response.data && err.response.data.error) || err.message), "error");
211
234
  closeTab(paneId, path);
212
235
  });
@@ -170,6 +170,29 @@ module Mbeditor
170
170
  return render json: { error: "Not found" }, status: :not_found
171
171
  end
172
172
 
173
+ start_line = params[:start_line] ? params[:start_line].to_i : nil
174
+ line_count = params.key?(:line_count) ? params[:line_count].to_i : 500
175
+ line_count = [line_count, 5000].min
176
+
177
+ if start_line
178
+ total_bytes = File.size(path)
179
+ chunk = []
180
+ total_lines = 0
181
+ File.foreach(path, encoding: "UTF-8", invalid: :replace, undef: :replace) do |line|
182
+ chunk << line if total_lines >= start_line && chunk.length < line_count
183
+ total_lines += 1
184
+ end
185
+ return render json: {
186
+ path: relative_path(path),
187
+ content: chunk.join,
188
+ truncated: total_lines > start_line + chunk.length,
189
+ start_line: start_line,
190
+ line_count: chunk.length,
191
+ total_lines: total_lines,
192
+ total_bytes: total_bytes
193
+ }
194
+ end
195
+
173
196
  size = File.size(path)
174
197
  return render_file_too_large(size) if size > MAX_OPEN_FILE_SIZE_BYTES
175
198
 
@@ -314,7 +337,8 @@ module Mbeditor
314
337
  workspace_root,
315
338
  symbol,
316
339
  excluded_dirnames: excluded_dirnames,
317
- excluded_paths: excluded_paths
340
+ excluded_paths: excluded_paths,
341
+ included_dirs: ruby_def_include_dirs
318
342
  )
319
343
  ri = RiDefinitionService.call(symbol)
320
344
  workspace + ri
@@ -337,7 +361,8 @@ module Mbeditor
337
361
  file = RubyDefinitionService.module_defined_in(
338
362
  workspace_root, name,
339
363
  excluded_dirnames: excluded_dirnames,
340
- excluded_paths: excluded_paths
364
+ excluded_paths: excluded_paths,
365
+ included_dirs: ruby_def_include_dirs
341
366
  )
342
367
  return render json: { name: name, methods: [] } unless file
343
368
 
@@ -360,14 +385,16 @@ module Mbeditor
360
385
  # Fast no-op on subsequent calls (mtime checks only).
361
386
  RubyDefinitionService.scan(workspace_root,
362
387
  excluded_dirnames: excluded_dirnames,
363
- excluded_paths: excluded_paths)
388
+ excluded_paths: excluded_paths,
389
+ included_dirs: ruby_def_include_dirs)
364
390
 
365
391
  module_names = RubyDefinitionService.includes_in_file(path)
366
392
  includes = module_names.filter_map do |mod_name|
367
393
  mod_file = RubyDefinitionService.module_defined_in(
368
394
  workspace_root, mod_name,
369
395
  excluded_dirnames: excluded_dirnames,
370
- excluded_paths: excluded_paths
396
+ excluded_paths: excluded_paths,
397
+ included_dirs: ruby_def_include_dirs
371
398
  )
372
399
  next unless mod_file
373
400
 
@@ -1045,7 +1072,8 @@ module Mbeditor
1045
1072
  if File.directory?(full)
1046
1073
  { name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
1047
1074
  else
1048
- { name: name, type: "file", path: rel }
1075
+ size = File.size(full) rescue nil
1076
+ { name: name, type: "file", path: rel, size: size }
1049
1077
  end
1050
1078
  end
1051
1079
  rescue Errno::EACCES
@@ -1060,6 +1088,10 @@ module Mbeditor
1060
1088
  excluded_paths.filter { |path| !path.include?("/") }
1061
1089
  end
1062
1090
 
1091
+ def ruby_def_include_dirs
1092
+ Array(Mbeditor.configuration.ruby_def_include_dirs).map(&:to_s).reject(&:blank?)
1093
+ end
1094
+
1063
1095
  def excluded_path?(relative_path, name)
1064
1096
  excluded_paths.any? do |pattern|
1065
1097
  if pattern.include?("/")
@@ -51,10 +51,11 @@ module Mbeditor
51
51
  attr_reader :file_cache, :mutex
52
52
  attr_accessor :cache_path
53
53
 
54
- def call(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
54
+ def call(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [], included_dirs: [])
55
55
  new(workspace_root, symbol,
56
56
  excluded_dirnames: excluded_dirnames,
57
- excluded_paths: excluded_paths).call
57
+ excluded_paths: excluded_paths,
58
+ included_dirs: included_dirs).call
58
59
  end
59
60
 
60
61
  # Load the JSON cache from disk exactly once per process (double-checked
@@ -122,20 +123,26 @@ module Mbeditor
122
123
  # Searches the cache (and triggers a workspace scan if needed) to find
123
124
  # which file in +workspace_root+ defines the given module or class name.
124
125
  # Returns the absolute file path string or nil.
125
- def module_defined_in(workspace_root, module_name, excluded_dirnames: [], excluded_paths: [])
126
+ def module_defined_in(workspace_root, module_name, excluded_dirnames: [], excluded_paths: [], included_dirs: [])
126
127
  load_disk_cache_once
128
+ root_prefix = workspace_root.to_s.chomp("/")
129
+ within_dirs = ->(path) {
130
+ return true if included_dirs.empty?
131
+ included_dirs.any? { |d| path.start_with?(File.join(root_prefix, d) + "/") }
132
+ }
127
133
  result = @mutex.synchronize do
128
- @file_cache.find { |_path, entry| entry[:module_names]&.include?(module_name) }
134
+ @file_cache.find { |path, entry| within_dirs.call(path) && entry[:module_names]&.include?(module_name) }
129
135
  end
130
136
  return result[0] if result
131
137
 
132
138
  # Cache miss: scan workspace to populate cache entries with module_names.
133
139
  new(workspace_root, nil,
134
140
  excluded_dirnames: excluded_dirnames,
135
- excluded_paths: excluded_paths).scan_workspace
141
+ excluded_paths: excluded_paths,
142
+ included_dirs: included_dirs).scan_workspace
136
143
 
137
144
  result = @mutex.synchronize do
138
- @file_cache.find { |_path, entry| entry[:module_names]&.include?(module_name) }
145
+ @file_cache.find { |path, entry| within_dirs.call(path) && entry[:module_names]&.include?(module_name) }
139
146
  end
140
147
  result ? result[0] : nil
141
148
  end
@@ -153,18 +160,20 @@ module Mbeditor
153
160
 
154
161
  # Convenience wrapper: scan the whole workspace to warm the cache.
155
162
  # Fast on subsequent calls (only re-parses files whose mtime changed).
156
- def scan(workspace_root, excluded_dirnames: [], excluded_paths: [])
163
+ def scan(workspace_root, excluded_dirnames: [], excluded_paths: [], included_dirs: [])
157
164
  new(workspace_root, nil,
158
165
  excluded_dirnames: excluded_dirnames,
159
- excluded_paths: excluded_paths).scan_workspace
166
+ excluded_paths: excluded_paths,
167
+ included_dirs: included_dirs).scan_workspace
160
168
  end
161
169
  end
162
170
 
163
- def initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
164
- @workspace_root = workspace_root.to_s.chomp("/")
165
- @symbol = symbol
171
+ def initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [], included_dirs: [])
172
+ @workspace_root = workspace_root.to_s.chomp("/")
173
+ @symbol = symbol
166
174
  @excluded_dirnames = Array(excluded_dirnames)
167
175
  @excluded_paths = Array(excluded_paths)
176
+ @included_dirs = Array(included_dirs)
168
177
  end
169
178
 
170
179
  # Walks the entire workspace and populates the per-file cache (including the
@@ -176,27 +185,29 @@ module Mbeditor
176
185
  files_scanned = 0
177
186
  evict_deleted_cache_entries
178
187
 
179
- Find.find(@workspace_root) do |path|
180
- if File.directory?(path)
181
- dirname = File.basename(path)
182
- rel_dir = relative_path(path)
183
- Find.prune if path != @workspace_root && excluded_dir?(dirname, rel_dir)
184
- next
185
- end
186
- next unless path.end_with?(".rb")
188
+ search_roots.each do |root|
189
+ Find.find(root) do |path|
190
+ if File.directory?(path)
191
+ dirname = File.basename(path)
192
+ rel_dir = relative_path(path)
193
+ Find.prune if path != root && excluded_dir?(dirname, rel_dir)
194
+ next
195
+ end
196
+ next unless path.end_with?(".rb")
187
197
 
188
- rel = relative_path(path)
189
- next if excluded_rel_path?(rel, File.basename(path))
198
+ rel = relative_path(path)
199
+ next if excluded_rel_path?(rel, File.basename(path))
190
200
 
191
- files_scanned += 1
192
- if files_scanned > MAX_FILES_SCANNED
193
- Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
194
- break
195
- end
196
- begin
197
- cache_entry_for(path)
198
- rescue StandardError
199
- nil
201
+ files_scanned += 1
202
+ if files_scanned > MAX_FILES_SCANNED
203
+ Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
204
+ break
205
+ end
206
+ begin
207
+ cache_entry_for(path)
208
+ rescue StandardError
209
+ nil
210
+ end
200
211
  end
201
212
  end
202
213
 
@@ -212,46 +223,46 @@ module Mbeditor
212
223
 
213
224
  evict_deleted_cache_entries
214
225
 
215
- Find.find(@workspace_root) do |path|
216
- # Prune excluded directories
217
- if File.directory?(path)
218
- dirname = File.basename(path)
219
- rel_dir = relative_path(path)
220
- if path != @workspace_root && excluded_dir?(dirname, rel_dir)
221
- Find.prune
226
+ search_roots.each do |root|
227
+ Find.find(root) do |path|
228
+ # Prune excluded directories
229
+ if File.directory?(path)
230
+ dirname = File.basename(path)
231
+ rel_dir = relative_path(path)
232
+ Find.prune if path != root && excluded_dir?(dirname, rel_dir)
233
+ next
222
234
  end
223
- next
224
- end
225
235
 
226
- next unless path.end_with?(".rb")
236
+ next unless path.end_with?(".rb")
227
237
 
228
- rel = relative_path(path)
229
- next if excluded_rel_path?(rel, File.basename(path))
230
-
231
- files_scanned += 1
232
- if files_scanned > MAX_FILES_SCANNED
233
- Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
234
- break
235
- end
238
+ rel = relative_path(path)
239
+ next if excluded_rel_path?(rel, File.basename(path))
236
240
 
237
- begin
238
- cached = cache_entry_for(path)
239
- next unless cached
240
-
241
- hit_lines = @symbol ? cached[:all_defs].fetch(@symbol, nil) : nil
242
- next unless hit_lines && hit_lines.any?
241
+ files_scanned += 1
242
+ if files_scanned > MAX_FILES_SCANNED
243
+ Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
244
+ break
245
+ end
243
246
 
244
- hit_lines.each do |def_line|
245
- results << {
246
- file: rel,
247
- line: def_line,
248
- signature: (cached[:lines][def_line - 1] || "").strip,
249
- comments: extract_comments(cached[:lines], def_line)
250
- }
251
- return results if results.length >= MAX_RESULTS
247
+ begin
248
+ cached = cache_entry_for(path)
249
+ next unless cached
250
+
251
+ hit_lines = @symbol ? cached[:all_defs].fetch(@symbol, nil) : nil
252
+ next unless hit_lines && hit_lines.any?
253
+
254
+ hit_lines.each do |def_line|
255
+ results << {
256
+ file: rel,
257
+ line: def_line,
258
+ signature: (cached[:lines][def_line - 1] || "").strip,
259
+ comments: extract_comments(cached[:lines], def_line)
260
+ }
261
+ return results if results.length >= MAX_RESULTS
262
+ end
263
+ rescue StandardError
264
+ # Malformed file or unreadable; skip silently
252
265
  end
253
- rescue StandardError
254
- # Malformed file or unreadable; skip silently
255
266
  end
256
267
  end
257
268
 
@@ -386,6 +397,15 @@ module Mbeditor
386
397
  full_path.to_s.delete_prefix(@workspace_root).delete_prefix("/")
387
398
  end
388
399
 
400
+ def search_roots
401
+ return [@workspace_root] if @included_dirs.empty?
402
+
403
+ dirs = @included_dirs
404
+ .map { |d| File.join(@workspace_root, d) }
405
+ .select { |d| File.directory?(d) }
406
+ dirs.empty? ? [@workspace_root] : dirs
407
+ end
408
+
389
409
  def excluded_dir?(dirname, rel_dir)
390
410
  @excluded_dirnames.include?(dirname) ||
391
411
  @excluded_paths.any? do |pattern|
@@ -6,7 +6,8 @@ module Mbeditor
6
6
  :redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
7
7
  :test_framework, :test_command, :test_timeout,
8
8
  :authenticate_with,
9
- :lint_timeout, :base_branch_candidates, :git_timeout
9
+ :lint_timeout, :base_branch_candidates, :git_timeout,
10
+ :ruby_def_include_dirs
10
11
 
11
12
  def initialize
12
13
  @allowed_environments = [:development]
@@ -22,7 +23,8 @@ module Mbeditor
22
23
  @test_timeout = 60 # seconds
23
24
  @lint_timeout = 15 # seconds for RuboCop/haml-lint subprocesses
24
25
  @base_branch_candidates = %w[origin/develop origin/main origin/master develop main master]
25
- @git_timeout = nil # seconds; nil disables (no timeout on git subprocesses)
26
+ @git_timeout = nil # seconds; nil disables (no timeout on git subprocesses)
27
+ @ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
26
28
  end
27
29
  end
28
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan