mbeditor 0.5.6 → 0.6.0
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 +20 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +53 -28
- data/app/assets/javascripts/mbeditor/components/FileTree.js +168 -116
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +361 -84
- data/app/assets/javascripts/mbeditor/editor_plugins.js +32 -4
- data/app/assets/javascripts/mbeditor/file_service.js +7 -2
- data/app/assets/javascripts/mbeditor/git_service.js +2 -1
- data/app/assets/javascripts/mbeditor/tab_manager.js +1 -1
- data/app/assets/stylesheets/mbeditor/editor.css +97 -65
- data/app/controllers/mbeditor/editors_controller.rb +21 -8
- data/app/services/mbeditor/rails_related_files_service.rb +234 -0
- data/config/routes.rb +1 -0
- data/lib/mbeditor/configuration.rb +2 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cecf19b27261ccaf639f6023af904a53a1084eba7b0f9d6104839fa2acfabea
|
|
4
|
+
data.tar.gz: 8a09361f3b23aa30d02a99362346be3f8ca8305db7d4c75cfa21362bbfe21ceb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a52538cb6ab034375dd94f3add4a9d8041b7993de09526a7319ee790189c0516f9636517662455ee03ddfb3f7795d99419ad9259ea92ac95fe766199e7a347f
|
|
7
|
+
data.tar.gz: e952248bbde28aa3021fc31099c9e763e424b7075aad41ceec7081faf4a761a31b1e704bfd5d5c9d780d4dbb3be4623eb5b6fb7c6036ebe5ebbfba1dcd9ffce4
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.6.0] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Virtual scrolling in FileTree** — only visible rows are rendered, dramatically improving performance with large file trees.
|
|
12
|
+
- **Multi-resource Rails panel** — the sidebar Rails panel now shows related files for up to 10 open Rails resources simultaneously, grouped by resource name with a dirty-file dot indicator.
|
|
13
|
+
- **Vim `:split` / `:vsplit` commands** — split the current file into the other pane using standard Vim command-line syntax.
|
|
14
|
+
- **Format changed-line highlighting** — after running Format Document, changed lines are highlighted with a green background for 3 seconds.
|
|
15
|
+
- **Sidebar panel titles** — "Explorer", "Search", and "Rails" labels now appear at the top of each sidebar panel.
|
|
16
|
+
- **Dirty-file indicators in Rails panel** — a small dot appears next to related files with unsaved changes.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Rails activity bar icon updated from text "R" to a Font Awesome gem icon.
|
|
20
|
+
- Monaco model cache size raised from 15 to 25 (`MAX_MODELS`).
|
|
21
|
+
- `wordBasedSuggestions` default changed to `'currentDocument'`; `linkedEditing` now respects user preferences.
|
|
22
|
+
- Git file comparison uses a lightweight signature function instead of `JSON.stringify`.
|
|
23
|
+
- Window-globals detection improved: symbols found on `window` at runtime are declared as globals to suppress TS2304 errors.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
- JSON auto-pretty-print on file open (files now display raw content).
|
|
27
|
+
|
|
8
28
|
## [0.5.1] - 2026-04-30
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -524,17 +524,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
524
524
|
// Evict the LRU model if the cache is at capacity before creating a new one.
|
|
525
525
|
TabManager.evictLruModel();
|
|
526
526
|
|
|
527
|
-
|
|
528
|
-
var contentForModel = tab.content;
|
|
529
|
-
if (language === 'json' && contentForModel) {
|
|
530
|
-
try {
|
|
531
|
-
contentForModel = JSON.stringify(JSON.parse(contentForModel), null, 2);
|
|
532
|
-
} catch (_) {
|
|
533
|
-
// invalid JSON — use raw content; Monaco will show error markers
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
modelObj = window.monaco.editor.createModel(contentForModel, language);
|
|
527
|
+
modelObj = window.monaco.editor.createModel(tab.content, language);
|
|
538
528
|
window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null, lastAccessed: Date.now(), cleanVersionId: null };
|
|
539
529
|
_modelEntry = window.__mbeditorModels[tab.path];
|
|
540
530
|
}
|
|
@@ -571,9 +561,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
571
561
|
formatOnPaste: editorPrefs.formatOnPaste !== false,
|
|
572
562
|
formatOnType: editorPrefs.formatOnType !== false,
|
|
573
563
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
574
|
-
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || '
|
|
564
|
+
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
575
565
|
acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on',
|
|
576
|
-
linkedEditing:
|
|
566
|
+
linkedEditing: !!(editorPrefs.linkedEditing),
|
|
577
567
|
fixedOverflowWidgets: true,
|
|
578
568
|
hover: { above: false }
|
|
579
569
|
});
|
|
@@ -713,7 +703,12 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
713
703
|
} else if (currentAvi > aviMaxRef.current) {
|
|
714
704
|
aviMaxRef.current = currentAvi;
|
|
715
705
|
}
|
|
716
|
-
|
|
706
|
+
var newCanUndo = currentAvi > aviBaseRef.current;
|
|
707
|
+
var newCanRedo = currentAvi < aviMaxRef.current;
|
|
708
|
+
var _st = EditorStore.getState();
|
|
709
|
+
if (_st.canUndo !== newCanUndo || _st.canRedo !== newCanRedo) {
|
|
710
|
+
EditorStore.setState({ canUndo: newCanUndo, canRedo: newCanRedo });
|
|
711
|
+
}
|
|
717
712
|
|
|
718
713
|
var val = editor.getValue();
|
|
719
714
|
|
|
@@ -819,18 +814,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
819
814
|
var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
820
815
|
if (_initEntry) _initEntry.cleanVersionId = null;
|
|
821
816
|
|
|
822
|
-
|
|
823
|
-
var contentToSet = tab.content;
|
|
824
|
-
var modelLang = model.getLanguageId();
|
|
825
|
-
if (modelLang === 'json' && contentToSet) {
|
|
826
|
-
try {
|
|
827
|
-
contentToSet = JSON.stringify(JSON.parse(contentToSet), null, 2);
|
|
828
|
-
} catch (_) {
|
|
829
|
-
// invalid JSON — use raw content; Monaco will show error markers
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
editor.setValue(contentToSet);
|
|
817
|
+
editor.setValue(tab.content);
|
|
834
818
|
// Reset the AVI baseline: setValue clears the undo stack so anything before
|
|
835
819
|
// this point is no longer reachable. Also clear the canUndo/canRedo display.
|
|
836
820
|
var newBase = model.getAlternativeVersionId();
|
|
@@ -935,7 +919,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
935
919
|
formatOnPaste: editorPrefs.formatOnPaste !== false,
|
|
936
920
|
formatOnType: editorPrefs.formatOnType !== false,
|
|
937
921
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
938
|
-
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || '
|
|
922
|
+
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
923
|
+
linkedEditing: !!(editorPrefs.linkedEditing),
|
|
939
924
|
acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on'
|
|
940
925
|
});
|
|
941
926
|
}
|
|
@@ -970,6 +955,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
970
955
|
});
|
|
971
956
|
MonacoVim.VimMode.Vim.map('<C-p>', ':mbeditorquickopen<CR>', 'normal');
|
|
972
957
|
MonacoVim.VimMode.Vim.map('<C-p>', ':mbeditorquickopen<CR>', 'visual');
|
|
958
|
+
// :split / :vsplit — open the current file in the other pane and focus it.
|
|
959
|
+
// Both map to the same behaviour since the editor always has exactly two panes.
|
|
960
|
+
var openInOtherPane = function() {
|
|
961
|
+
var model = monacoRef.current && monacoRef.current.getModel();
|
|
962
|
+
var filePath = model && model._mbeditorPath;
|
|
963
|
+
if (!filePath || typeof TabManager === 'undefined') return;
|
|
964
|
+
var otherPaneId = paneId === 1 ? 2 : 1;
|
|
965
|
+
// forcePaneId (4th arg) bypasses the "redirect empty pane 2 → pane 1" guard.
|
|
966
|
+
TabManager.openTab(filePath, filePath.split('/').pop(), null, otherPaneId);
|
|
967
|
+
TabManager.focusPane(otherPaneId);
|
|
968
|
+
window.dispatchEvent(new CustomEvent('mbeditor:focusPane', { detail: { paneId: otherPaneId } }));
|
|
969
|
+
};
|
|
970
|
+
MonacoVim.VimMode.Vim.defineEx('split', 'sp', openInOtherPane);
|
|
971
|
+
MonacoVim.VimMode.Vim.defineEx('vsplit', 'vs', openInOtherPane);
|
|
973
972
|
vimModeObjRef.current = vimInstance;
|
|
974
973
|
});
|
|
975
974
|
} else {
|
|
@@ -987,6 +986,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
987
986
|
};
|
|
988
987
|
}, [editorPrefs.vimMode]);
|
|
989
988
|
|
|
989
|
+
// Focus this pane's Monaco editor when MbeditorApp dispatches mbeditor:focusPane
|
|
990
|
+
// (used by the vim Ctrl+W panel-switching handler to move keyboard focus).
|
|
991
|
+
useEffect(function() {
|
|
992
|
+
function onFocusPane(e) {
|
|
993
|
+
if (e.detail && e.detail.paneId === paneId && monacoRef.current) {
|
|
994
|
+
monacoRef.current.focus();
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
window.addEventListener('mbeditor:focusPane', onFocusPane);
|
|
998
|
+
return function() { window.removeEventListener('mbeditor:focusPane', onFocusPane); };
|
|
999
|
+
}, [paneId]);
|
|
1000
|
+
|
|
990
1001
|
// Jump to line if specified
|
|
991
1002
|
useEffect(function () {
|
|
992
1003
|
if (tab.gotoLine && monacoRef.current) {
|
|
@@ -1831,4 +1842,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1831
1842
|
);
|
|
1832
1843
|
};
|
|
1833
1844
|
|
|
1834
|
-
|
|
1845
|
+
// treeData and all function props (onSave, onFormat, etc.) are intentionally
|
|
1846
|
+
// excluded — their references change every parent render but don't affect editor output.
|
|
1847
|
+
window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
|
|
1848
|
+
return prev.tab === next.tab &&
|
|
1849
|
+
prev.paneId === next.paneId &&
|
|
1850
|
+
(prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
|
|
1851
|
+
prev.gitAvailable === next.gitAvailable &&
|
|
1852
|
+
prev.testAvailable === next.testAvailable &&
|
|
1853
|
+
prev.testResult === next.testResult &&
|
|
1854
|
+
prev.testPanelFile === next.testPanelFile &&
|
|
1855
|
+
prev.testLoading === next.testLoading &&
|
|
1856
|
+
prev.testInlineVisible === next.testInlineVisible &&
|
|
1857
|
+
prev.editorPrefs === next.editorPrefs &&
|
|
1858
|
+
prev.monacoReady === next.monacoReady;
|
|
1859
|
+
});
|
|
@@ -69,12 +69,40 @@ var FileTree = function FileTree(_ref) {
|
|
|
69
69
|
// signal is where they last clicked.
|
|
70
70
|
var sidebarActiveRef = useRef(false);
|
|
71
71
|
|
|
72
|
+
// Virtual scroll state
|
|
73
|
+
var ROW_HEIGHT = 22;
|
|
74
|
+
var BUFFER = 5;
|
|
75
|
+
var flatItemsRef = useRef([]);
|
|
76
|
+
var scrollParentRef = useRef(null);
|
|
77
|
+
|
|
78
|
+
var _scrollState = useState(0);
|
|
79
|
+
var scrollTop = _scrollState[0];
|
|
80
|
+
var setScrollTop = _scrollState[1];
|
|
81
|
+
|
|
82
|
+
var _heightState = useState(400);
|
|
83
|
+
var containerHeight = _heightState[0];
|
|
84
|
+
var setContainerHeight = _heightState[1];
|
|
85
|
+
|
|
86
|
+
// Scroll item at idx into view via the parent scroll container
|
|
87
|
+
var scrollToItem = function scrollToItem(idx) {
|
|
88
|
+
var parent = scrollParentRef.current;
|
|
89
|
+
if (!parent || !containerRef.current) return;
|
|
90
|
+
var parentTop = parent.getBoundingClientRect().top;
|
|
91
|
+
var treeTop = containerRef.current.getBoundingClientRect().top;
|
|
92
|
+
var treeOffset = parent.scrollTop + (treeTop - parentTop);
|
|
93
|
+
var itemAbsTop = treeOffset + idx * ROW_HEIGHT;
|
|
94
|
+
var itemAbsBottom = itemAbsTop + ROW_HEIGHT;
|
|
95
|
+
if (itemAbsTop < parent.scrollTop || itemAbsBottom > parent.scrollTop + parent.clientHeight) {
|
|
96
|
+
parent.scrollTop = Math.max(0, itemAbsTop - Math.floor((parent.clientHeight - ROW_HEIGHT) / 2));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
72
100
|
// Scroll the highlighted node into view when anchorPath changes (e.g. Find in Explorer)
|
|
73
101
|
useEffect(function () {
|
|
74
|
-
if (!anchorPath
|
|
102
|
+
if (!anchorPath) return;
|
|
75
103
|
var timer = setTimeout(function () {
|
|
76
|
-
var
|
|
77
|
-
if (
|
|
104
|
+
var idx = flatItemsRef.current.findIndex(function(item) { return item.kind === 'node' && item.node.path === anchorPath; });
|
|
105
|
+
if (idx >= 0) scrollToItem(idx);
|
|
78
106
|
}, 60);
|
|
79
107
|
return function () { clearTimeout(timer); };
|
|
80
108
|
}, [anchorPath]);
|
|
@@ -97,15 +125,8 @@ var FileTree = function FileTree(_ref) {
|
|
|
97
125
|
|
|
98
126
|
// After the DOM updates, scroll the active item into view only if not already visible
|
|
99
127
|
var timer = setTimeout(function () {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (el) {
|
|
103
|
-
var elRect = el.getBoundingClientRect();
|
|
104
|
-
var containerRect = containerRef.current.getBoundingClientRect();
|
|
105
|
-
if (elRect.top < containerRect.top || elRect.bottom > containerRect.bottom) {
|
|
106
|
-
el.scrollIntoView({ block: 'nearest' });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
128
|
+
var idx = flatItemsRef.current.findIndex(function(item) { return item.kind === 'node' && item.node.path === activePath; });
|
|
129
|
+
if (idx >= 0) scrollToItem(idx);
|
|
109
130
|
}, 80);
|
|
110
131
|
return function () { clearTimeout(timer); };
|
|
111
132
|
}, [activePath]);
|
|
@@ -134,6 +155,8 @@ var FileTree = function FileTree(_ref) {
|
|
|
134
155
|
if (pendingCreate) {
|
|
135
156
|
setInlineValue('');
|
|
136
157
|
committedRef.current = false;
|
|
158
|
+
var createIdx = flatItemsRef.current.findIndex(function(item) { return item.kind === 'create'; });
|
|
159
|
+
if (createIdx >= 0) scrollToItem(createIdx);
|
|
137
160
|
setTimeout(function () {
|
|
138
161
|
if (inlineRef.current) inlineRef.current.focus();
|
|
139
162
|
}, 0);
|
|
@@ -146,6 +169,35 @@ var FileTree = function FileTree(_ref) {
|
|
|
146
169
|
return function() { clearTimeout(hoverTimerRef.current); };
|
|
147
170
|
}, []);
|
|
148
171
|
|
|
172
|
+
// Track the parent scroll container (.ide-sidebar-scrollable) for virtual scroll.
|
|
173
|
+
// The file-tree-root itself does not scroll — the parent does.
|
|
174
|
+
useEffect(function() {
|
|
175
|
+
if (!containerRef.current) return;
|
|
176
|
+
var parent = containerRef.current.closest('.ide-sidebar-scrollable');
|
|
177
|
+
if (!parent) return;
|
|
178
|
+
scrollParentRef.current = parent;
|
|
179
|
+
|
|
180
|
+
var update = function() {
|
|
181
|
+
if (!containerRef.current || !parent) return;
|
|
182
|
+
var parentRect = parent.getBoundingClientRect();
|
|
183
|
+
var treeRect = containerRef.current.getBoundingClientRect();
|
|
184
|
+
var treeOffset = parent.scrollTop + (treeRect.top - parentRect.top);
|
|
185
|
+
setScrollTop(Math.max(0, parent.scrollTop - treeOffset));
|
|
186
|
+
setContainerHeight(parent.clientHeight);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
parent.addEventListener('scroll', update, { passive: true });
|
|
190
|
+
var obs = new ResizeObserver(update);
|
|
191
|
+
obs.observe(parent);
|
|
192
|
+
obs.observe(containerRef.current);
|
|
193
|
+
update();
|
|
194
|
+
|
|
195
|
+
return function() {
|
|
196
|
+
parent.removeEventListener('scroll', update);
|
|
197
|
+
obs.disconnect();
|
|
198
|
+
};
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
149
201
|
var toggleFolder = function toggleFolder(path, e) {
|
|
150
202
|
e.stopPropagation();
|
|
151
203
|
var next = !(expandedDirs && expandedDirs[path]);
|
|
@@ -186,22 +238,10 @@ var FileTree = function FileTree(_ref) {
|
|
|
186
238
|
};
|
|
187
239
|
|
|
188
240
|
// Returns all visible paths in depth-first render order (for shift+click range select)
|
|
189
|
-
var computeVisiblePaths = function computeVisiblePaths(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
|
194
|
-
return a.name.localeCompare(b.name);
|
|
195
|
-
});
|
|
196
|
-
sorted.forEach(function (node) {
|
|
197
|
-
paths.push(node.path);
|
|
198
|
-
if (node.type === 'folder' && expandedDirs && expandedDirs[node.path] && node.children) {
|
|
199
|
-
visit(node.children);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
};
|
|
203
|
-
visit(nodes);
|
|
204
|
-
return paths;
|
|
241
|
+
var computeVisiblePaths = function computeVisiblePaths(_nodes) {
|
|
242
|
+
return flatItemsRef.current
|
|
243
|
+
.filter(function(item) { return item.kind === 'node'; })
|
|
244
|
+
.map(function(item) { return item.node.path; });
|
|
205
245
|
};
|
|
206
246
|
|
|
207
247
|
// Global type-ahead: tracks the last mousedown to know if the user is "in the explorer",
|
|
@@ -232,17 +272,14 @@ var FileTree = function FileTree(_ref) {
|
|
|
232
272
|
}, 600);
|
|
233
273
|
|
|
234
274
|
var prefix = typeaheadBufferRef.current;
|
|
235
|
-
var
|
|
236
|
-
for (var i = 0; i <
|
|
237
|
-
var
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
var nodeType = allItems[i].getAttribute('data-type');
|
|
244
|
-
if (nodePath && onNodeSelectRef.current) {
|
|
245
|
-
onNodeSelectRef.current({ path: nodePath, name: nodeName || nodePath.split('/').pop(), type: nodeType || 'file' });
|
|
275
|
+
var flat = flatItemsRef.current;
|
|
276
|
+
for (var i = 0; i < flat.length; i++) {
|
|
277
|
+
var fItem = flat[i];
|
|
278
|
+
if (fItem.kind !== 'node') continue;
|
|
279
|
+
if (fItem.node.name.toLowerCase().indexOf(prefix) === 0) {
|
|
280
|
+
scrollToItem(i);
|
|
281
|
+
if (onNodeSelectRef.current) {
|
|
282
|
+
onNodeSelectRef.current({ path: fItem.node.path, name: fItem.node.name, type: fItem.node.type });
|
|
246
283
|
}
|
|
247
284
|
break;
|
|
248
285
|
}
|
|
@@ -348,60 +385,93 @@ var FileTree = function FileTree(_ref) {
|
|
|
348
385
|
);
|
|
349
386
|
};
|
|
350
387
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
388
|
+
// Flatten the visible tree into a linear array for virtual scrolling.
|
|
389
|
+
var flattenVisible = function flattenVisible(nodes, depth, parentPath) {
|
|
390
|
+
var result = [];
|
|
391
|
+
var sorted = [].concat(_toConsumableArray(nodes)).sort(function(a, b) {
|
|
392
|
+
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
|
354
393
|
return a.name.localeCompare(b.name);
|
|
355
394
|
});
|
|
395
|
+
sorted.forEach(function(node) {
|
|
396
|
+
result.push({ kind: 'node', node: node, depth: depth });
|
|
397
|
+
if (node.type === 'folder' && expandedDirs && expandedDirs[node.path] && node.children) {
|
|
398
|
+
var children = flattenVisible(node.children, depth + 1, node.path);
|
|
399
|
+
for (var ci = 0; ci < children.length; ci++) result.push(children[ci]);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
if (pendingCreate && pendingCreate.parentPath === parentPath) {
|
|
403
|
+
result.push({ kind: 'create', depth: depth });
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
var flatItems = flattenVisible(items || [], 0, '');
|
|
409
|
+
flatItemsRef.current = flatItems;
|
|
410
|
+
|
|
411
|
+
var totalHeight = flatItems.length * ROW_HEIGHT;
|
|
412
|
+
var startIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER);
|
|
413
|
+
var endIdx = Math.min(flatItems.length - 1, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + BUFFER);
|
|
356
414
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var isExpanded = !!(expandedDirs && expandedDirs[node.path]);
|
|
360
|
-
var isRenamingThisNode = !!(pendingRename && pendingRename.path === node.path);
|
|
361
|
-
var isOpenFile = activePath === node.path;
|
|
362
|
-
var isSelected = !!(selectedPaths && selectedPaths.has(node.path));
|
|
363
|
-
var isDragOver = isFolder && dragOverFolder === node.path;
|
|
364
|
-
var status = getGitStatus(node.path);
|
|
365
|
-
var statusMeta = getTreeStatusMeta(status);
|
|
366
|
-
var isModified = statusMeta && (statusMeta.cssKey === "M" || statusMeta.cssKey === "A");
|
|
367
|
-
|
|
368
|
-
var classNames = 'tree-item' +
|
|
369
|
-
(isOpenFile ? ' active' : '') +
|
|
370
|
-
(isSelected ? ' selected' : '') +
|
|
371
|
-
(isModified ? ' modified' : '') +
|
|
372
|
-
(isDragOver ? ' drag-over' : '');
|
|
415
|
+
var renderRow = function renderRow(item, idx) {
|
|
416
|
+
var indentPx = 8 + item.depth * 12;
|
|
373
417
|
|
|
418
|
+
if (item.kind === 'create') {
|
|
374
419
|
return React.createElement(
|
|
375
420
|
'div',
|
|
376
|
-
{ key:
|
|
377
|
-
|
|
421
|
+
{ key: '__inline-create__', style: { position: 'absolute', top: idx * ROW_HEIGHT, left: 0, right: 0 } },
|
|
422
|
+
React.createElement('div', { style: { paddingLeft: indentPx } }, renderInlineRow())
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
var node = item.node;
|
|
427
|
+
var isFolder = node.type === 'folder';
|
|
428
|
+
var isExpanded = !!(expandedDirs && expandedDirs[node.path]);
|
|
429
|
+
var isRenamingThisNode = !!(pendingRename && pendingRename.path === node.path);
|
|
430
|
+
var isOpenFile = activePath === node.path;
|
|
431
|
+
var isSelected = !!(selectedPaths && selectedPaths.has(node.path));
|
|
432
|
+
var isDragOver = isFolder && dragOverFolder === node.path;
|
|
433
|
+
var status = getGitStatus(node.path);
|
|
434
|
+
var statusMeta = getTreeStatusMeta(status);
|
|
435
|
+
var isModified = statusMeta && (statusMeta.cssKey === 'M' || statusMeta.cssKey === 'A');
|
|
436
|
+
|
|
437
|
+
var classNames = 'tree-item' +
|
|
438
|
+
(isOpenFile ? ' active' : '') +
|
|
439
|
+
(isSelected ? ' selected' : '') +
|
|
440
|
+
(isModified ? ' modified' : '') +
|
|
441
|
+
(isDragOver ? ' drag-over' : '');
|
|
442
|
+
|
|
443
|
+
return React.createElement(
|
|
444
|
+
'div',
|
|
445
|
+
{ key: node.path, style: { position: 'absolute', top: idx * ROW_HEIGHT, left: 0, right: 0 } },
|
|
446
|
+
isRenamingThisNode
|
|
447
|
+
? React.createElement('div', { style: { paddingLeft: indentPx } }, renderInlineRenameRow(node))
|
|
448
|
+
: React.createElement(
|
|
378
449
|
'div',
|
|
379
450
|
{
|
|
380
451
|
className: classNames,
|
|
452
|
+
style: { paddingLeft: indentPx },
|
|
381
453
|
'data-path': node.path,
|
|
382
454
|
'data-name': node.name,
|
|
383
455
|
'data-type': node.type,
|
|
384
456
|
draggable: true,
|
|
385
|
-
onDragStart: function
|
|
386
|
-
// If the dragged node is part of a multi-selection, drag all selected; otherwise just this node
|
|
457
|
+
onDragStart: function(e) {
|
|
387
458
|
var srcPaths = (selectedPaths && selectedPaths.has(node.path) && selectedPaths.size > 1)
|
|
388
459
|
? Array.from(selectedPaths)
|
|
389
460
|
: [node.path];
|
|
390
461
|
e.dataTransfer.setData('text/plain', JSON.stringify(srcPaths));
|
|
391
462
|
e.dataTransfer.effectAllowed = 'move';
|
|
392
463
|
},
|
|
393
|
-
onDragOver: function
|
|
464
|
+
onDragOver: function(e) {
|
|
394
465
|
if (!isFolder) return;
|
|
395
466
|
e.preventDefault();
|
|
396
467
|
e.stopPropagation();
|
|
397
468
|
e.dataTransfer.dropEffect = 'move';
|
|
398
469
|
if (dragOverFolder !== node.path) setDragOverFolder(node.path);
|
|
399
470
|
},
|
|
400
|
-
onDragLeave: function
|
|
401
|
-
// Only clear if we're leaving the folder item itself, not entering a child
|
|
471
|
+
onDragLeave: function() {
|
|
402
472
|
if (dragOverFolder === node.path) setDragOverFolder(null);
|
|
403
473
|
},
|
|
404
|
-
onDrop: function
|
|
474
|
+
onDrop: function(e) {
|
|
405
475
|
e.preventDefault();
|
|
406
476
|
e.stopPropagation();
|
|
407
477
|
setDragOverFolder(null);
|
|
@@ -411,66 +481,51 @@ var FileTree = function FileTree(_ref) {
|
|
|
411
481
|
if (onMove && srcPaths && srcPaths.length > 0) onMove(srcPaths, node.path);
|
|
412
482
|
} catch (err) {}
|
|
413
483
|
},
|
|
414
|
-
onDragEnd: function
|
|
415
|
-
onClick: function
|
|
484
|
+
onDragEnd: function() { setDragOverFolder(null); },
|
|
485
|
+
onClick: function(e) {
|
|
416
486
|
if (e.ctrlKey || e.metaKey) {
|
|
417
|
-
// Ctrl/Cmd+click: toggle this node in/out of selection
|
|
418
487
|
if (onMultiSelect) {
|
|
419
488
|
var newPaths = new Set(selectedPaths || []);
|
|
420
|
-
if (newPaths.has(node.path)) {
|
|
421
|
-
newPaths.delete(node.path);
|
|
422
|
-
} else {
|
|
423
|
-
newPaths.add(node.path);
|
|
424
|
-
}
|
|
489
|
+
if (newPaths.has(node.path)) { newPaths.delete(node.path); } else { newPaths.add(node.path); }
|
|
425
490
|
onMultiSelect(newPaths);
|
|
426
491
|
}
|
|
427
|
-
// Don't open file on ctrl-click
|
|
428
492
|
} else if (e.shiftKey && anchorPath) {
|
|
429
|
-
// Shift+click: range-select from anchor to this node
|
|
430
493
|
if (onMultiSelect) {
|
|
431
494
|
var visiblePaths = computeVisiblePaths(items);
|
|
432
|
-
var
|
|
433
|
-
var
|
|
434
|
-
if (
|
|
435
|
-
var start = Math.min(
|
|
436
|
-
var end = Math.max(
|
|
495
|
+
var anchorIdx2 = visiblePaths.indexOf(anchorPath);
|
|
496
|
+
var currentIdx2 = visiblePaths.indexOf(node.path);
|
|
497
|
+
if (anchorIdx2 >= 0 && currentIdx2 >= 0) {
|
|
498
|
+
var start = Math.min(anchorIdx2, currentIdx2);
|
|
499
|
+
var end = Math.max(anchorIdx2, currentIdx2);
|
|
437
500
|
onMultiSelect(new Set(visiblePaths.slice(start, end + 1)));
|
|
438
501
|
} else {
|
|
439
502
|
selectNode(node);
|
|
440
503
|
}
|
|
441
504
|
}
|
|
442
505
|
} else {
|
|
443
|
-
// Normal single click
|
|
444
506
|
selectNode(node);
|
|
445
|
-
if (isFolder) {
|
|
446
|
-
toggleFolder(node.path, e);
|
|
447
|
-
} else {
|
|
448
|
-
onSelect(node.path, node.name);
|
|
449
|
-
}
|
|
507
|
+
if (isFolder) { toggleFolder(node.path, e); } else { onSelect(node.path, node.name); }
|
|
450
508
|
if (containerRef.current) containerRef.current.focus();
|
|
451
509
|
}
|
|
452
510
|
},
|
|
453
|
-
onDoubleClick: function
|
|
454
|
-
if (!isFolder && onFileDoubleClick) {
|
|
455
|
-
e.stopPropagation();
|
|
456
|
-
onFileDoubleClick(node.path, node.name);
|
|
457
|
-
}
|
|
511
|
+
onDoubleClick: function(e) {
|
|
512
|
+
if (!isFolder && onFileDoubleClick) { e.stopPropagation(); onFileDoubleClick(node.path, node.name); }
|
|
458
513
|
},
|
|
459
|
-
onMouseEnter: function
|
|
514
|
+
onMouseEnter: function() {
|
|
460
515
|
if (isFolder) return;
|
|
461
516
|
clearTimeout(hoverTimerRef.current);
|
|
462
|
-
hoverTimerRef.current = setTimeout(function
|
|
517
|
+
hoverTimerRef.current = setTimeout(function() {
|
|
463
518
|
hoverPathRef.current = node.path;
|
|
464
519
|
FileService.prefetch(node.path);
|
|
465
520
|
}, 200);
|
|
466
521
|
},
|
|
467
|
-
onMouseLeave: function
|
|
522
|
+
onMouseLeave: function() {
|
|
468
523
|
if (isFolder) return;
|
|
469
524
|
clearTimeout(hoverTimerRef.current);
|
|
470
525
|
FileService.cancelPrefetch(hoverPathRef.current);
|
|
471
526
|
hoverPathRef.current = null;
|
|
472
527
|
},
|
|
473
|
-
onContextMenu: function
|
|
528
|
+
onContextMenu: function(e) {
|
|
474
529
|
e.preventDefault();
|
|
475
530
|
e.stopPropagation();
|
|
476
531
|
selectNode(node);
|
|
@@ -478,9 +533,10 @@ var FileTree = function FileTree(_ref) {
|
|
|
478
533
|
}
|
|
479
534
|
},
|
|
480
535
|
React.createElement(
|
|
481
|
-
'div',
|
|
482
|
-
|
|
483
|
-
|
|
536
|
+
'div', { className: 'tree-item-icon' },
|
|
537
|
+
isFolder
|
|
538
|
+
? React.createElement('i', { className: 'fas fa-folder' + (isExpanded ? '-open' : '') + ' tree-folder-icon' })
|
|
539
|
+
: React.createElement('i', { className: window.getFileIcon(node.name) + ' tree-file-icon' })
|
|
484
540
|
),
|
|
485
541
|
React.createElement(
|
|
486
542
|
'div',
|
|
@@ -492,27 +548,23 @@ var FileTree = function FileTree(_ref) {
|
|
|
492
548
|
{ className: 'git-status-badge git-' + statusMeta.cssKey, title: statusMeta.title },
|
|
493
549
|
statusMeta.badge
|
|
494
550
|
)
|
|
495
|
-
),
|
|
496
|
-
isFolder && isExpanded && node.children && React.createElement(
|
|
497
|
-
'div',
|
|
498
|
-
{ style: { paddingLeft: "12px" } },
|
|
499
|
-
renderTree(node.children, node.path)
|
|
500
551
|
)
|
|
501
|
-
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// Inject inline create row at the end of this directory's list
|
|
505
|
-
if (pendingCreate && pendingCreate.parentPath === folderPath) {
|
|
506
|
-
rows.push(renderInlineRow());
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return rows;
|
|
552
|
+
);
|
|
510
553
|
};
|
|
511
554
|
|
|
555
|
+
var visibleRows = [];
|
|
556
|
+
for (var vi = startIdx; vi <= endIdx; vi++) {
|
|
557
|
+
visibleRows.push(renderRow(flatItems[vi], vi));
|
|
558
|
+
}
|
|
559
|
+
|
|
512
560
|
return React.createElement(
|
|
513
561
|
'div',
|
|
514
|
-
{ className: 'file-tree-root', ref: containerRef, tabIndex: 0, style: { outline: 'none' } },
|
|
515
|
-
|
|
562
|
+
{ className: 'file-tree file-tree-root', ref: containerRef, tabIndex: 0, style: { outline: 'none', padding: 0 } },
|
|
563
|
+
React.createElement(
|
|
564
|
+
'div',
|
|
565
|
+
{ style: { height: totalHeight, position: 'relative' } },
|
|
566
|
+
visibleRows
|
|
567
|
+
)
|
|
516
568
|
);
|
|
517
569
|
};
|
|
518
570
|
|
|
@@ -527,7 +579,7 @@ var FileTreeMemo = React.memo(FileTree, function(prev, next) {
|
|
|
527
579
|
prev.activePath === next.activePath &&
|
|
528
580
|
prev.selectedPaths === next.selectedPaths &&
|
|
529
581
|
prev.anchorPath === next.anchorPath &&
|
|
530
|
-
|
|
582
|
+
prev.gitFiles === next.gitFiles &&
|
|
531
583
|
prev.expandedDirs === next.expandedDirs &&
|
|
532
584
|
prev.pendingCreate === next.pendingCreate &&
|
|
533
585
|
prev.pendingRename === next.pendingRename;
|