mbeditor 0.4.5 → 0.5.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: 75e7d28ba5664e0aeae526a954b2032c040d6ac2f20ed327bef9507a29bc3e21
4
- data.tar.gz: 34b2ba82856f0fef274049050a55e95a465b9d9bdb7e86e3a2299c30ce0e00c4
3
+ metadata.gz: 79b3c0567fff4f540eeca0a01d4f5133192e55b200b920808295c443b67d5f6a
4
+ data.tar.gz: 54f7fa3e433a9dbd34b17556fa0abfb244b220b4800af1997bea0be700955b22
5
5
  SHA512:
6
- metadata.gz: ba333da50a99c900136a52832821b7a2d939f7836b5aa05d2bdaedb259167c3c8a32f7ec3e13e854501c51a7686dd354973b3ff9a299c87af2b0e41cfaa8a649
7
- data.tar.gz: 9fd083956de7ac0c3443715ba15fe63af069822043ab9e4113b8ab9af4096e5ac582e29d8377cd63f1c681b9fbcca5097dbc7f1d20592f937df6bb208fe91988
6
+ metadata.gz: 1f2e2cec02b0019326c1e16e3659da31c9d6387fd84c8d38e0ca72be1ea007e22b4e5fc26512caa0a620f30ca589718e149bab210a1eeef17f349ba452289c1d
7
+ data.tar.gz: bd145f73f0df001904fa139a8365f9dae815c9ad391bc8bc77aaefd0cb64a6317f8c74f0d43bdabcbab6c5878a6cba7b0155219dceb0b57d038814bd96096864
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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.0] - 2026-04-30
9
+
10
+ ### Added
11
+ - **Zen / focus mode** — `Cmd+Shift+Z` hides the sidebar and git panel for a distraction-free editing experience.
12
+ - **Bulk find-and-replace** — search and replace across all workspace files in one operation.
13
+ - **File content prefetch on hover** — opening a file from the tree is now instant; content is fetched while hovering the row.
14
+ - **Client-side search cache** — search results are cached client-side for 30 s and invalidated automatically on save.
15
+ - **Monaco model cache with LRU eviction** — in-memory Monaco models are capped at 15 and evicted in LRU order to keep memory use bounded.
16
+
17
+ ### Changed
18
+ - **Faster initial render** — Monaco startup is now decoupled from the React mount lifecycle, cutting time-to-first-edit.
19
+ - **Robust dirty-state tracking** — dirty state is now driven by Monaco's `alternativeVersionId`; `cleanVersionId` is reset correctly on save-on-close so re-opened tabs start clean.
20
+
21
+ ### Fixed
22
+ - ActionCable reconnect logic hardened; regression tests added to confirm websocket lifecycle log filtering still works after reconnect.
23
+ - Bulk replace: fixed a security issue and a correctness bug in the replacement pipeline; added covering tests.
24
+
8
25
  ## [0.4.5] - 2026-04-23
9
26
 
10
27
  ### Fixed
@@ -1,3 +1,4 @@
1
1
  window.SearchService = SearchService;
2
2
  window.GitService = GitService;
3
+ window.FileService = FileService;
3
4
  })(window.MbeditorRuntime.React, window.MbeditorRuntime.ReactDOM);
@@ -30,6 +30,7 @@ var EditorPanel = function EditorPanel(_ref) {
30
30
  var testLoading = _ref.testLoading;
31
31
  var testInlineVisible = _ref.testInlineVisible;
32
32
  var editorPrefs = _ref.editorPrefs || {};
33
+ var monacoReady = _ref.monacoReady !== false; // undefined means Monaco already loaded (legacy callers)
33
34
 
34
35
  var editorRef = useRef(null);
35
36
  var monacoRef = useRef(null);
@@ -132,7 +133,7 @@ var EditorPanel = function EditorPanel(_ref) {
132
133
 
133
134
  useEffect(function () {
134
135
  if (tab.isPreview) return;
135
- if (!editorRef.current || !window.monaco) return;
136
+ if (!monacoReady || !editorRef.current || !window.monaco) return;
136
137
 
137
138
  if (window.MbeditorEditorPlugins && window.MbeditorEditorPlugins.registerGlobalExtensions) {
138
139
  window.MbeditorEditorPlugins.registerGlobalExtensions(window.monaco);
@@ -478,13 +479,17 @@ var EditorPanel = function EditorPanel(_ref) {
478
479
  if (_modelEntry && _modelEntry.model && !_modelEntry.model.isDisposed()) {
479
480
  modelObj = _modelEntry.model;
480
481
  _reusingModel = true;
482
+ // Update access timestamp so LRU eviction knows this model was recently used.
483
+ _modelEntry.lastAccessed = Date.now();
481
484
  // Re-apply language in case it changed (e.g. file renamed)
482
485
  if (modelObj.getLanguageId() !== language) {
483
486
  window.monaco.editor.setModelLanguage(modelObj, language);
484
487
  }
485
488
  } else {
489
+ // Evict the LRU model if the cache is at capacity before creating a new one.
490
+ TabManager.evictLruModel();
486
491
  modelObj = window.monaco.editor.createModel(tab.content, language);
487
- window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null };
492
+ window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null, lastAccessed: Date.now(), cleanVersionId: null };
488
493
  _modelEntry = window.__mbeditorModels[tab.path];
489
494
  }
490
495
 
@@ -645,6 +650,8 @@ var EditorPanel = function EditorPanel(_ref) {
645
650
  } else {
646
651
  aviBaseRef.current = avi;
647
652
  aviMaxRef.current = avi;
653
+ // Record the clean baseline for dirty-state tracking on initial model creation.
654
+ _modelEntry.cleanVersionId = avi;
648
655
  }
649
656
  EditorStore.setState({ canUndo: avi > aviBaseRef.current, canRedo: avi < aviMaxRef.current });
650
657
 
@@ -660,19 +667,16 @@ var EditorPanel = function EditorPanel(_ref) {
660
667
 
661
668
  var val = editor.getValue();
662
669
 
663
- // Belt-and-suspenders: when undoing, directly check if content matches the
664
- // saved clean state. This guarantees the dirty flag clears on full undo even
665
- // if the latestContentRef comparison path misses an event.
666
- if (e.isUndoing) {
667
- var _st = EditorStore.getState();
668
- var _pane = _st.panes.find(function(p) { return p.id === paneId; });
669
- var _tab = _pane && _pane.tabs.find(function(t) { return t.id === tab.id; });
670
- if (_tab && _tab.dirty) {
671
- var _cleanNorm = ((_tab.cleanContent) || '').replace(/\r\n/g, '\n');
672
- var _valNorm = val.replace(/\r\n/g, '\n');
673
- if (_valNorm === _cleanNorm) {
674
- TabManager.markClean(paneId, tab.id, val);
675
- }
670
+ // Dirty-state tracking via alternativeVersionId O(1), no string comparison.
671
+ // AVI decrements on undo so it returns to cleanVersionId after a full undo.
672
+ // Skip entirely when cleanVersionId is null file is mid-load, not yet settled.
673
+ var _entry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
674
+ var _cleanAvi = _entry && _entry.cleanVersionId;
675
+ if (_cleanAvi !== null && _cleanAvi !== undefined) {
676
+ if (currentAvi !== _cleanAvi) {
677
+ TabManager.markDirty(paneId, tab.id, val);
678
+ } else {
679
+ TabManager.markClean(paneId, tab.id, val);
676
680
  }
677
681
  }
678
682
 
@@ -734,7 +738,7 @@ var EditorPanel = function EditorPanel(_ref) {
734
738
  editor.setModel(null);
735
739
  editor.dispose();
736
740
  };
737
- }, [tab.id, tab.isPreview]); // re-run ONLY on tab switch, not on content change (Monaco handles its own content state)
741
+ }, [tab.id, tab.isPreview, monacoReady]); // re-run on tab switch or when Monaco becomes ready
738
742
 
739
743
  // Listen for external content changes (e.g. after Format/Load)
740
744
  // Only applies when externalContentVersion advances — prevents stale typing-originated
@@ -760,12 +764,17 @@ var EditorPanel = function EditorPanel(_ref) {
760
764
  if (!vNorm) {
761
765
  // If the editor is currently completely empty, treat it as an initial load.
762
766
  // setValue clears the undo stack which is correct for initial load.
767
+ // Null cleanVersionId before setValue so the synchronous onDidChangeContent
768
+ // fires during setValue and skips the dirty check (cleanVersionId is null).
769
+ var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
770
+ if (_initEntry) _initEntry.cleanVersionId = null;
763
771
  editor.setValue(tab.content);
764
772
  // Reset the AVI baseline: setValue clears the undo stack so anything before
765
773
  // this point is no longer reachable. Also clear the canUndo/canRedo display.
766
774
  var newBase = model.getAlternativeVersionId();
767
775
  aviBaseRef.current = newBase;
768
776
  aviMaxRef.current = newBase;
777
+ if (_initEntry) _initEntry.cleanVersionId = newBase;
769
778
  EditorStore.setState({ canUndo: false, canRedo: false });
770
779
  } else {
771
780
  // Keep undo stack for formats or replaces by using executeEdits
@@ -1363,6 +1372,16 @@ var EditorPanel = function EditorPanel(_ref) {
1363
1372
  return '\u2026/' + parts.slice(-2).join('/');
1364
1373
  }
1365
1374
 
1375
+ // While Monaco is still loading, show a lightweight skeleton so the UI is
1376
+ // visible immediately without calling monaco.editor.create() too early.
1377
+ if (!monacoReady) {
1378
+ return React.createElement(
1379
+ 'div',
1380
+ { className: 'monaco-container monaco-loading-skeleton', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#888', fontSize: '13px' } },
1381
+ 'Loading editor…'
1382
+ );
1383
+ }
1384
+
1366
1385
  // Always render the same wrapper structure so the editorRef div is never
1367
1386
  // unmounted when gitAvailable changes (e.g. loaded async after workspace
1368
1387
  // call returns). The toolbar is conditionally included inside the wrapper.
@@ -51,6 +51,8 @@ var FileTree = function FileTree(_ref) {
51
51
  var containerRef = useRef(null);
52
52
  var typeaheadBufferRef = useRef('');
53
53
  var typeaheadTimerRef = useRef(null);
54
+ var hoverTimerRef = useRef(null);
55
+ var hoverPathRef = useRef(null);
54
56
  // Ref that always points to the latest onNodeSelect prop, avoiding stale closures in the effect.
55
57
  var onNodeSelectRef = useRef(onNodeSelect);
56
58
  onNodeSelectRef.current = onNodeSelect;
@@ -131,6 +133,12 @@ var FileTree = function FileTree(_ref) {
131
133
  }
132
134
  }, [pendingCreate, pendingRename]);
133
135
 
136
+ // Clear any pending hover timer when the component unmounts to prevent
137
+ // a prefetch from firing against an unmounted component.
138
+ useEffect(function() {
139
+ return function() { clearTimeout(hoverTimerRef.current); };
140
+ }, []);
141
+
134
142
  var toggleFolder = function toggleFolder(path, e) {
135
143
  e.stopPropagation();
136
144
  var next = !(expandedDirs && expandedDirs[path]);
@@ -441,6 +449,20 @@ var FileTree = function FileTree(_ref) {
441
449
  onFileDoubleClick(node.path, node.name);
442
450
  }
443
451
  },
452
+ onMouseEnter: function () {
453
+ if (isFolder) return;
454
+ clearTimeout(hoverTimerRef.current);
455
+ hoverTimerRef.current = setTimeout(function () {
456
+ hoverPathRef.current = node.path;
457
+ FileService.prefetch(node.path);
458
+ }, 200);
459
+ },
460
+ onMouseLeave: function () {
461
+ if (isFolder) return;
462
+ clearTimeout(hoverTimerRef.current);
463
+ FileService.cancelPrefetch(hoverPathRef.current);
464
+ hoverPathRef.current = null;
465
+ },
444
466
  onContextMenu: function (e) {
445
467
  e.preventDefault();
446
468
  e.stopPropagation();
@@ -498,7 +520,7 @@ var FileTreeMemo = React.memo(FileTree, function(prev, next) {
498
520
  prev.activePath === next.activePath &&
499
521
  prev.selectedPaths === next.selectedPaths &&
500
522
  prev.anchorPath === next.anchorPath &&
501
- prev.gitFiles === next.gitFiles &&
523
+ JSON.stringify(prev.gitFiles) === JSON.stringify(next.gitFiles) &&
502
524
  prev.expandedDirs === next.expandedDirs &&
503
525
  prev.pendingCreate === next.pendingCreate &&
504
526
  prev.pendingRename === next.pendingRename;