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 +4 -4
- data/CHANGELOG.md +6 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +170 -2
- data/app/assets/javascripts/mbeditor/components/FileTree.js +8 -1
- data/app/assets/javascripts/mbeditor/editor_plugins.js +115 -22
- data/app/assets/javascripts/mbeditor/file_service.js +9 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +23 -0
- data/app/controllers/mbeditor/editors_controller.rb +37 -5
- data/app/services/mbeditor/ruby_definition_service.rb +84 -64
- data/lib/mbeditor/configuration.rb +4 -2
- data/lib/mbeditor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd0837fbeccc3634d7804e6f9957bbbaca06bb4c02942eb4decc5e8b174499ab
|
|
4
|
+
data.tar.gz: 9f97239369b2968c17cd3909abdced4917329a349564d02ebde6beb89cb8c36f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 { |
|
|
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
|
|
141
|
+
excluded_paths: excluded_paths,
|
|
142
|
+
included_dirs: included_dirs).scan_workspace
|
|
136
143
|
|
|
137
144
|
result = @mutex.synchronize do
|
|
138
|
-
@file_cache.find { |
|
|
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
|
|
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
|
|
165
|
-
@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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
198
|
+
rel = relative_path(path)
|
|
199
|
+
next if excluded_rel_path?(rel, File.basename(path))
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
236
|
+
next unless path.end_with?(".rb")
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
data/lib/mbeditor/version.rb
CHANGED