mbeditor 0.3.5 → 0.3.8
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 +44 -6
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +19 -7
- data/app/assets/javascripts/mbeditor/editor_plugins.js +3 -3
- data/app/assets/javascripts/mbeditor/tab_manager.js +15 -0
- data/app/assets/stylesheets/mbeditor/editor.css +56 -0
- data/app/controllers/mbeditor/editors_controller.rb +2 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: caf178914c2be95307a23ee24744dabd50f12c8597c69eddf814f63895e17433
|
|
4
|
+
data.tar.gz: 0353fc7e791cf1fff4c739fddf59d0a1ba8259353a4147b738824275c4bca55f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 772a81a73df9b7ca2bd517193ee0178fb9843ea934c7bbc05157e84794596c62b98f34dbfa50004e2c8ee1abeda1e121e8936350826b1a213031952c1ef10563
|
|
7
|
+
data.tar.gz: 705bc59cb210934c909b35819d41c98ecd3e858a153a25efa6587e530c51faa0821aa39932c155c4860c7335a0c70d7f06302e4c6a5b7cb7f1214911bb1f7fbe
|
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.3.8] - 2026-04-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Prevented Emmet's custom Tab action from intercepting selected text, so multi-line selections are no longer replaced when pressing Tab/Shift+Tab in markup editors.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Added a system regression test to ensure multi-line selections in JSX are not collapsed by the Emmet Tab integration.
|
|
15
|
+
|
|
16
|
+
## [0.3.7] - 2026-04-16
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Corrected the `destroy_path` missing-path controller test to expect `200 OK` for idempotent DELETE behavior, which unblocks the tag-based publish workflow for this release.
|
|
20
|
+
|
|
21
|
+
## [0.3.6] - 2026-04-15
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Multi-file/folder delete now deduplicates child paths covered by a selected ancestor directory, preventing redundant requests and Rails 404 console errors.
|
|
25
|
+
- Switched multi-delete from `Promise.all` to `Promise.allSettled` so all deletions complete before checking for failures rather than bailing on the first rejection.
|
|
26
|
+
- `destroy_path` controller action is now idempotent — returns 200 when the path is already gone, matching correct REST DELETE semantics.
|
|
27
|
+
|
|
8
28
|
## [0.2.8] - 2026-04-15
|
|
9
29
|
|
|
10
30
|
### Changed
|
|
@@ -451,9 +451,32 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
451
451
|
if (language === 'image') return;
|
|
452
452
|
|
|
453
453
|
lastAppliedExternalVersionRef.current = -1;
|
|
454
|
+
|
|
455
|
+
// Look up or create a persistent Monaco model for this file path.
|
|
456
|
+
// Reusing the model across tab switches preserves the undo/redo history.
|
|
457
|
+
if (!window.__mbeditorModels) window.__mbeditorModels = {};
|
|
458
|
+
var _modelEntry = window.__mbeditorModels[tab.path];
|
|
459
|
+
var _reusingModel = false;
|
|
460
|
+
var modelObj;
|
|
461
|
+
if (_modelEntry && _modelEntry.model && !_modelEntry.model.isDisposed()) {
|
|
462
|
+
modelObj = _modelEntry.model;
|
|
463
|
+
_reusingModel = true;
|
|
464
|
+
// Re-apply language in case it changed (e.g. file renamed)
|
|
465
|
+
if (modelObj.getLanguageId() !== language) {
|
|
466
|
+
window.monaco.editor.setModelLanguage(modelObj, language);
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
modelObj = window.monaco.editor.createModel(tab.content, language);
|
|
470
|
+
window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null };
|
|
471
|
+
_modelEntry = window.__mbeditorModels[tab.path];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Sync latestContentRef from the actual model content so the onDidChangeContent
|
|
475
|
+
// handler doesn't fire a spurious onContentChange call on the first keystroke.
|
|
476
|
+
latestContentRef.current = _reusingModel ? modelObj.getValue() : (tab.content || '');
|
|
477
|
+
|
|
454
478
|
var editor = window.monaco.editor.create(editorRef.current, {
|
|
455
|
-
|
|
456
|
-
language: language,
|
|
479
|
+
model: modelObj,
|
|
457
480
|
theme: editorPrefs.theme || 'vs-dark',
|
|
458
481
|
automaticLayout: true,
|
|
459
482
|
minimap: { enabled: !!(editorPrefs.minimap) },
|
|
@@ -487,7 +510,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
487
510
|
|
|
488
511
|
// Stash the workspace-relative path on the model so code-action providers
|
|
489
512
|
// can identify which file they are operating on without needing React state.
|
|
490
|
-
var modelObj = editor.getModel();
|
|
491
513
|
if (modelObj) modelObj._mbeditorPath = tab.path;
|
|
492
514
|
|
|
493
515
|
var formatActionDisposable = editor.addAction({
|
|
@@ -547,10 +569,17 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
547
569
|
|
|
548
570
|
// Track undo/redo availability via Monaco's alternativeVersionId.
|
|
549
571
|
// AVI goes up on every edit, down on undo, back up on redo.
|
|
572
|
+
// When reusing an existing model, restore the saved AVI thresholds so the
|
|
573
|
+
// canUndo/canRedo buttons reflect the real state of the undo stack.
|
|
550
574
|
var avi = modelObj.getAlternativeVersionId();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
575
|
+
if (_reusingModel && _modelEntry.aviBase !== null) {
|
|
576
|
+
aviBaseRef.current = _modelEntry.aviBase;
|
|
577
|
+
aviMaxRef.current = _modelEntry.aviMax !== null ? _modelEntry.aviMax : avi;
|
|
578
|
+
} else {
|
|
579
|
+
aviBaseRef.current = avi;
|
|
580
|
+
aviMaxRef.current = avi;
|
|
581
|
+
}
|
|
582
|
+
EditorStore.setState({ canUndo: avi > aviBaseRef.current, canRedo: avi < aviMaxRef.current });
|
|
554
583
|
|
|
555
584
|
var contentDisposable = modelObj.onDidChangeContent(function (e) {
|
|
556
585
|
var currentAvi = modelObj.getAlternativeVersionId();
|
|
@@ -599,6 +628,12 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
599
628
|
clearBlameZones(editor);
|
|
600
629
|
clearTestZones(editor);
|
|
601
630
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
631
|
+
// Persist AVI thresholds on the model entry so undo/redo state is correct on return.
|
|
632
|
+
var _me = window.__mbeditorModels && window.__mbeditorModels[tab.path];
|
|
633
|
+
if (_me) {
|
|
634
|
+
_me.aviBase = aviBaseRef.current;
|
|
635
|
+
_me.aviMax = aviMaxRef.current;
|
|
636
|
+
}
|
|
602
637
|
if (window.__mbeditorActiveEditor === editor) {
|
|
603
638
|
window.__mbeditorActiveEditor = null;
|
|
604
639
|
}
|
|
@@ -607,6 +642,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
607
642
|
columnSelectDisposable.dispose();
|
|
608
643
|
contentDisposable.dispose();
|
|
609
644
|
EditorStore.setState({ canUndo: false, canRedo: false });
|
|
645
|
+
// Detach the model before disposing the editor so the model (and its undo
|
|
646
|
+
// history) survives for when the user returns to this tab.
|
|
647
|
+
editor.setModel(null);
|
|
610
648
|
editor.dispose();
|
|
611
649
|
};
|
|
612
650
|
}, [tab.id, tab.isPreview]); // re-run ONLY on tab switch, not on content change (Monaco handles its own content state)
|
|
@@ -2000,6 +2000,15 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2000
2000
|
return;
|
|
2001
2001
|
}
|
|
2002
2002
|
|
|
2003
|
+
// Remove paths that are already covered by a selected ancestor directory.
|
|
2004
|
+
// This prevents redundant requests (and resulting 404s) when a folder and
|
|
2005
|
+
// its children are both in the selection.
|
|
2006
|
+
pathsToDelete = pathsToDelete.filter(function(p) {
|
|
2007
|
+
return !pathsToDelete.some(function(other) {
|
|
2008
|
+
return other !== p && p.startsWith(other.endsWith('/') ? other : other + '/');
|
|
2009
|
+
});
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2003
2012
|
var label = pathsToDelete.length === 1 ? pathsToDelete[0] : pathsToDelete.length + ' items';
|
|
2004
2013
|
var confirmed = window.confirm('Delete ' + label + '? This cannot be undone.');
|
|
2005
2014
|
if (!confirmed) return;
|
|
@@ -2007,19 +2016,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2007
2016
|
setLoading(function (prev) {
|
|
2008
2017
|
return _extends({}, prev, { deletePath: true });
|
|
2009
2018
|
});
|
|
2010
|
-
Promise.
|
|
2019
|
+
Promise.allSettled(pathsToDelete.map(function(p) {
|
|
2011
2020
|
return FileService.deletePath(p).then(function() {
|
|
2012
2021
|
removeDeletedPathFromOpenTabs(p);
|
|
2013
2022
|
});
|
|
2014
|
-
})).then(function () {
|
|
2015
|
-
|
|
2016
|
-
|
|
2023
|
+
})).then(function (results) {
|
|
2024
|
+
var failures = results.filter(function(r) { return r.status === 'rejected'; });
|
|
2025
|
+
if (failures.length === 0) {
|
|
2026
|
+
handleNodeSelect(null);
|
|
2027
|
+
EditorStore.setStatus('Deleted: ' + label, 'success');
|
|
2028
|
+
} else {
|
|
2029
|
+
var message = failures[0].reason && failures[0].reason.response && failures[0].reason.response.data && failures[0].reason.response.data.error || (failures[0].reason && failures[0].reason.message) || 'Unknown error';
|
|
2030
|
+
EditorStore.setStatus('Delete failed: ' + message, 'error');
|
|
2031
|
+
}
|
|
2017
2032
|
return refreshProjectTree().then(function () {
|
|
2018
2033
|
GitService.fetchStatus();
|
|
2019
2034
|
});
|
|
2020
|
-
})["catch"](function (err) {
|
|
2021
|
-
var message = err && err.response && err.response.data && err.response.data.error || err.message;
|
|
2022
|
-
EditorStore.setStatus('Delete failed: ' + message, 'error');
|
|
2023
2035
|
})["finally"](function () {
|
|
2024
2036
|
setLoading(function (prev) {
|
|
2025
2037
|
return _extends({}, prev, { deletePath: false });
|
|
@@ -197,12 +197,12 @@
|
|
|
197
197
|
id: 'mbeditor.emmet.expandAbbreviation',
|
|
198
198
|
label: 'Emmet: Expand Abbreviation',
|
|
199
199
|
keybindings: [window.monaco.KeyCode.Tab],
|
|
200
|
-
precondition: '!suggestWidgetVisible && !parameterHintsVisible',
|
|
200
|
+
precondition: '!suggestWidgetVisible && !parameterHintsVisible && !editorHasSelection',
|
|
201
201
|
run: function(editor) {
|
|
202
202
|
var selection = editor.getSelection();
|
|
203
|
-
//
|
|
203
|
+
// Selection indentation should use Monaco defaults, not Emmet expansion.
|
|
204
204
|
if (!selection.isEmpty()) {
|
|
205
|
-
editor.trigger('keyboard', '
|
|
205
|
+
editor.trigger('keyboard', 'editor.action.indentLines', null);
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
var pos = editor.getPosition();
|
|
@@ -318,6 +318,21 @@ var TabManager = (function () {
|
|
|
318
318
|
focusedPaneId: nextFocusedPaneId,
|
|
319
319
|
activeTabId: maybeNewGlobalActiveTab
|
|
320
320
|
});
|
|
321
|
+
|
|
322
|
+
// Dispose the cached Monaco model for this path if it is no longer open in
|
|
323
|
+
// any pane. This keeps the model registry clean and frees memory.
|
|
324
|
+
if (window.__mbeditorModels && window.__mbeditorModels[path]) {
|
|
325
|
+
var _stillOpen = newPanes.some(function(p) {
|
|
326
|
+
return p.tabs.some(function(t) { return t.path === path; });
|
|
327
|
+
});
|
|
328
|
+
if (!_stillOpen) {
|
|
329
|
+
var _entry = window.__mbeditorModels[path];
|
|
330
|
+
if (_entry.model && !_entry.model.isDisposed()) {
|
|
331
|
+
_entry.model.dispose();
|
|
332
|
+
}
|
|
333
|
+
delete window.__mbeditorModels[path];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
321
336
|
}
|
|
322
337
|
|
|
323
338
|
function closeAllTabsInPane(paneId) {
|
|
@@ -816,6 +816,59 @@ html, body, #mbeditor-root {
|
|
|
816
816
|
.search-input-wrap {
|
|
817
817
|
display: flex;
|
|
818
818
|
flex-shrink: 0;
|
|
819
|
+
align-items: center;
|
|
820
|
+
gap: 4px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.search-input-shell {
|
|
824
|
+
flex: 1;
|
|
825
|
+
position: relative;
|
|
826
|
+
display: flex;
|
|
827
|
+
align-items: center;
|
|
828
|
+
min-width: 0;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.search-input-shell .search-input {
|
|
832
|
+
padding-right: 24px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/* search-clear-btn sits absolutely inside the input shell */
|
|
836
|
+
.search-input-shell .search-clear-btn {
|
|
837
|
+
position: absolute;
|
|
838
|
+
right: 4px;
|
|
839
|
+
width: 16px;
|
|
840
|
+
height: 16px;
|
|
841
|
+
font-size: 10px;
|
|
842
|
+
color: var(--ide-text-muted);
|
|
843
|
+
background: transparent;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* Use .search-panel ancestor for higher specificity than button:not(.pico-btn) */
|
|
847
|
+
.search-panel .search-btn {
|
|
848
|
+
flex-shrink: 0;
|
|
849
|
+
width: 26px;
|
|
850
|
+
height: 26px;
|
|
851
|
+
padding: 0;
|
|
852
|
+
margin: 0;
|
|
853
|
+
border: 1px solid var(--ide-border-input);
|
|
854
|
+
border-radius: 4px;
|
|
855
|
+
background: var(--ide-input-bg);
|
|
856
|
+
color: var(--ide-text-muted);
|
|
857
|
+
font-size: 12px;
|
|
858
|
+
display: inline-flex;
|
|
859
|
+
align-items: center;
|
|
860
|
+
justify-content: center;
|
|
861
|
+
cursor: pointer;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.search-panel .search-btn:hover {
|
|
865
|
+
color: var(--ide-text);
|
|
866
|
+
border-color: var(--ide-accent-fg);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.search-panel .search-btn:disabled {
|
|
870
|
+
opacity: 0.5;
|
|
871
|
+
cursor: not-allowed;
|
|
819
872
|
}
|
|
820
873
|
|
|
821
874
|
.search-input {
|
|
@@ -872,6 +925,9 @@ html, body, #mbeditor-root {
|
|
|
872
925
|
font-size: 11px;
|
|
873
926
|
color: var(--ide-accent-fg);
|
|
874
927
|
font-weight: 500;
|
|
928
|
+
white-space: nowrap;
|
|
929
|
+
overflow: hidden;
|
|
930
|
+
text-overflow: ellipsis;
|
|
875
931
|
}
|
|
876
932
|
|
|
877
933
|
.search-result-line-num {
|
|
@@ -240,7 +240,8 @@ module Mbeditor
|
|
|
240
240
|
def destroy_path
|
|
241
241
|
path = resolve_path(params[:path])
|
|
242
242
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
243
|
-
|
|
243
|
+
# Idempotent: if already gone the desired state is already achieved.
|
|
244
|
+
return render json: { ok: true } unless File.exist?(path)
|
|
244
245
|
return render json: { error: "Cannot delete this path" }, status: :forbidden if path_blocked_for_operations?(path)
|
|
245
246
|
|
|
246
247
|
if File.directory?(path)
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|