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 +4 -4
- data/CHANGELOG.md +17 -0
- data/app/assets/javascripts/mbeditor/application_iife_tail.js +1 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +35 -16
- data/app/assets/javascripts/mbeditor/components/FileTree.js +23 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +324 -48
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +173 -0
- data/app/assets/javascripts/mbeditor/file_service.js +84 -1
- data/app/assets/javascripts/mbeditor/git_service.js +7 -3
- data/app/assets/javascripts/mbeditor/search_service.js +91 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +55 -2
- data/app/assets/stylesheets/mbeditor/editor.css +29 -0
- data/app/controllers/mbeditor/editors_controller.rb +295 -41
- data/app/services/mbeditor/ruby_definition_service.rb +163 -21
- data/app/services/mbeditor/unused_methods_service.rb +139 -0
- data/app/views/layouts/mbeditor/application.html.erb +86 -56
- data/config/routes.rb +4 -0
- 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: 79b3c0567fff4f540eeca0a01d4f5133192e55b200b920808295c443b67d5f6a
|
|
4
|
+
data.tar.gz: 54f7fa3e433a9dbd34b17556fa0abfb244b220b4800af1997bea0be700955b22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -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
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
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;
|