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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acc982f027b14cfd3ad12a911b327edef848a353f11fc64df7b2c5e194557a45
4
- data.tar.gz: 768bf200ae935e14b4974249e41b27c26d2b2d105207a03a2399727f3b951912
3
+ metadata.gz: 3cecf19b27261ccaf639f6023af904a53a1084eba7b0f9d6104839fa2acfabea
4
+ data.tar.gz: 8a09361f3b23aa30d02a99362346be3f8ca8305db7d4c75cfa21362bbfe21ceb
5
5
  SHA512:
6
- metadata.gz: 43a3c2adfdaf54835bca24a81d2dfbfd668b85be02ea62113ce3bb038baafbe2f717e0651cdb9e9c40657548ef3ceb3e4ee94dbf5633baf6e267a4b92fefeabf
7
- data.tar.gz: 26fd951cc646560d6bdb2b0db9a02ef8f0f821e76232bfceea2c40dfcdb99d1768a8f2ddb5cc0efb037862899485845ecffe28274a5bcb9ff04e17f7fe049024
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
- // Pretty-print JSON content before initial load
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 || 'matchingDocuments',
564
+ wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
575
565
  acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on',
576
- linkedEditing: true,
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
- EditorStore.setState({ canUndo: currentAvi > aviBaseRef.current, canRedo: currentAvi < aviMaxRef.current });
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
- // Pretty-print JSON content before initial load
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 || 'matchingDocuments',
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
- window.EditorPanel = EditorPanel;
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 || !containerRef.current) return;
102
+ if (!anchorPath) return;
75
103
  var timer = setTimeout(function () {
76
- var el = containerRef.current && containerRef.current.querySelector('.tree-item.selected');
77
- if (el) el.scrollIntoView({ block: 'center' });
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
- if (!containerRef.current) return;
101
- var el = containerRef.current.querySelector('.tree-item.active');
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(nodes) {
190
- var paths = [];
191
- var visit = function visit(list) {
192
- var sorted = [].concat(_toConsumableArray(list)).sort(function (a, b) {
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 allItems = containerRef.current.querySelectorAll('.tree-item');
236
- for (var i = 0; i < allItems.length; i++) {
237
- var nameEl = allItems[i].querySelector('.tree-item-name');
238
- if (nameEl && nameEl.textContent.trim().toLowerCase().indexOf(prefix) === 0) {
239
- allItems[i].scrollIntoView({ block: 'nearest' });
240
- // Visually select the matched item so the user sees the highlight change.
241
- var nodePath = allItems[i].getAttribute('data-path');
242
- var nodeName = allItems[i].getAttribute('data-name');
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
- var renderTree = function renderTree(nodes, folderPath) {
352
- var sortedNodes = [].concat(_toConsumableArray(nodes)).sort(function (a, b) {
353
- if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
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
- var rows = sortedNodes.map(function (node) {
358
- var isFolder = node.type === "folder";
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: node.path, className: 'file-tree' },
377
- isRenamingThisNode ? renderInlineRenameRow(node) : React.createElement(
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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 () { setDragOverFolder(null); },
415
- onClick: function (e) {
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 anchorIdx = visiblePaths.indexOf(anchorPath);
433
- var currentIdx = visiblePaths.indexOf(node.path);
434
- if (anchorIdx >= 0 && currentIdx >= 0) {
435
- var start = Math.min(anchorIdx, currentIdx);
436
- var end = Math.max(anchorIdx, currentIdx);
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 (e) {
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 (e) {
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
- { className: 'tree-item-icon' },
483
- isFolder ? React.createElement('i', { className: 'fas fa-folder' + (isExpanded ? "-open" : "") + ' tree-folder-icon' }) : React.createElement('i', { className: window.getFileIcon(node.name) + ' tree-file-icon' })
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
- renderTree(items, '')
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
- JSON.stringify(prev.gitFiles) === JSON.stringify(next.gitFiles) &&
582
+ prev.gitFiles === next.gitFiles &&
531
583
  prev.expandedDirs === next.expandedDirs &&
532
584
  prev.pendingCreate === next.pendingCreate &&
533
585
  prev.pendingRename === next.pendingRename;