mbeditor 0.3.9 → 0.4.3
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 +38 -0
- data/README.md +18 -0
- data/app/assets/javascripts/mbeditor/application.js +1 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +161 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +392 -62
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +20 -4
- data/app/assets/javascripts/mbeditor/components/TabBar.js +3 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +21 -0
- data/app/assets/javascripts/mbeditor/file_service.js +6 -0
- data/app/assets/javascripts/mbeditor/search_service.js +7 -3
- data/app/assets/javascripts/mbeditor/websocket_service.js +195 -0
- data/app/assets/stylesheets/mbeditor/application.css +19 -0
- data/app/assets/stylesheets/mbeditor/editor.css +225 -10
- data/app/channels/mbeditor/editor_channel.rb +81 -0
- data/app/controllers/mbeditor/editors_controller.rb +65 -13
- data/app/views/layouts/mbeditor/application.html.erb +4 -0
- data/lib/mbeditor/cable_log_filter.rb +56 -0
- data/lib/mbeditor/engine.rb +10 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +4 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf6000382b991d7d35aa850c38fc80c743089c60f4a2277310f552e5a3b85477
|
|
4
|
+
data.tar.gz: 4d9c42bc71a9db592172eb09a601b134628f3c7f4d619a271349dda9b675c5ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e4b14f3d506a23c1f157a7dd91d4c522a568623fd555b3ef10e6e83e561b4bdcaf040b822ea1c2ec86ca1eed8f47992f7fa1060cb18a9600ee2f43b10de4730
|
|
7
|
+
data.tar.gz: '0911b0a22050d16d96a1e841ae16e73673fd0579549efd5590d538efe405ca66e7f1470597680e0102954b4d4ce6ef9337d02a22dc736c487998f990808ff34f'
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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.4.3] - 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Documented optional Action Cable host-app setup and fallback behavior for realtime editor updates.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `bundle exec rake test` now includes `test/lib/**/*_test.rb` so release validation covers library-level regression tests.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Action Cable availability detection now respects whether `/cable` is actually mounted, and websocket handshake failures fall back to polling without noisy console errors.
|
|
18
|
+
- Circular loading indicators keep animating even when global reduced-motion styles are active in production.
|
|
19
|
+
- `CableLogFilter` now remains compatible with untagged logger stacks used outside full Action Cable setups.
|
|
20
|
+
|
|
21
|
+
## [0.4.2] - 2026-04-22
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Updated sidebar search system test selectors to match the current UI loading and clear controls, resolving CI matrix failures in GitHub Actions.
|
|
25
|
+
|
|
26
|
+
## [0.4.1] - 2026-04-22
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Real-time file update integration via Action Cable to improve collaborative and live-refresh editing workflows.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Search functionality enhancements for improved result handling and responsiveness.
|
|
33
|
+
|
|
34
|
+
## [0.4.0] - 2026-04-22
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **Draft auto-save and restore** — unsaved edits are written to `localStorage` every 500 ms per file. On reconnect after a server outage, a dialog offers to restore any drafts that diverged from the in-memory tab content.
|
|
38
|
+
- **Tab bar layout setting** — new "Tab bar layout" preference (Scroll / Wrap multi-row) stored in editor prefs; `TabBar` receives a `tabDisplayMode` prop and applies the appropriate CSS class; horizontal wheel scroll is disabled in wrap mode.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- **Drag-to-split** — pane 2 drop zone is now only shown when the cursor actively hovers over the right-half content area, preventing the tab bar from collapsing during a drag. Dropping onto a tab bar element is no longer intercepted by the cross-pane drop handler, preserving same-pane reordering.
|
|
42
|
+
- **Quick Open sorting** — files consistently rank above directories; within the same priority tier results are sorted by match relevance (exact basename > prefix match > substring > other).
|
|
43
|
+
- **Search total count** — the count thread is joined with a 100 ms timeout; `total_count` is omitted from the response when the thread has not finished, so the first page is never blocked by the counting subprocess.
|
|
44
|
+
- **Status bar message colour** — removed `color-mix` transparency that was dimming the accent-text colour.
|
|
45
|
+
|
|
8
46
|
## [0.3.9] - 2026-04-21
|
|
9
47
|
|
|
10
48
|
### Added
|
data/README.md
CHANGED
|
@@ -121,9 +121,27 @@ The gem keeps host/tooling responsibilities in the host app:
|
|
|
121
121
|
- `haml_lint` gem (optional, required for HAML lint — add to your app's Gemfile if needed)
|
|
122
122
|
- `git` installed in environment (for Git panel data)
|
|
123
123
|
- `minitest` or `rspec` in the host app's bundle (required for the test runner)
|
|
124
|
+
- `actioncable` framework/gem (optional, required only for realtime file-change push + websocket state saves)
|
|
124
125
|
|
|
125
126
|
All lint and test tools are auto-detected at runtime. The engine gracefully disables features if the tools are not available. Neither `rubocop`, `haml_lint`, nor any test framework are runtime dependencies of the gem itself — they are discovered from the host app's environment.
|
|
126
127
|
|
|
128
|
+
### Realtime via Action Cable (Optional)
|
|
129
|
+
|
|
130
|
+
Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
|
|
131
|
+
|
|
132
|
+
To enable realtime features in a host app:
|
|
133
|
+
|
|
134
|
+
1. Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
|
|
135
|
+
2. Mount cable in host routes:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
mount ActionCable.server => '/cable'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
3. Make Action Cable JavaScript available to the page (for asset-pipeline apps, `actioncable.js` is typically sufficient).
|
|
142
|
+
|
|
143
|
+
If any of these are missing, mbeditor still runs in polling mode.
|
|
144
|
+
|
|
127
145
|
### Syntax Highlighting Support
|
|
128
146
|
Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
|
|
129
147
|
The gem includes syntax highlighting for common Rails and React development file types:
|
|
@@ -68,6 +68,23 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
68
68
|
var editorReady = _useState10[0];
|
|
69
69
|
var setEditorReady = _useState10[1];
|
|
70
70
|
|
|
71
|
+
var _useState11 = useState(false);
|
|
72
|
+
var _useState12 = _slicedToArray(_useState11, 2);
|
|
73
|
+
var methodsOpen = _useState12[0];
|
|
74
|
+
var setMethodsOpen = _useState12[1];
|
|
75
|
+
|
|
76
|
+
var _useState13 = useState([]);
|
|
77
|
+
var _useState14 = _slicedToArray(_useState13, 2);
|
|
78
|
+
var methodsList = _useState14[0];
|
|
79
|
+
var setMethodsList = _useState14[1];
|
|
80
|
+
|
|
81
|
+
var _useState15 = useState(null);
|
|
82
|
+
var _useState16 = _slicedToArray(_useState15, 2);
|
|
83
|
+
var methodsDropdownPos = _useState16[0];
|
|
84
|
+
var setMethodsDropdownPos = _useState16[1];
|
|
85
|
+
|
|
86
|
+
var methodsBtnRef = useRef(null);
|
|
87
|
+
|
|
71
88
|
var onFormatRef = useRef(onFormat);
|
|
72
89
|
onFormatRef.current = onFormat;
|
|
73
90
|
|
|
@@ -514,6 +531,46 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
514
531
|
editor.restoreViewState(tab.viewState);
|
|
515
532
|
}
|
|
516
533
|
|
|
534
|
+
// Restore the find widget state from the previous editor (when persistFindState is on).
|
|
535
|
+
if (editorPrefs.persistFindState !== false && window.__mbeditorFindState && window.__mbeditorFindState.searchString) {
|
|
536
|
+
var _savedFind = window.__mbeditorFindState;
|
|
537
|
+
if (_savedFind.isRevealed) {
|
|
538
|
+
// Open the widget first (setTimeout so layout is ready), then re-apply the
|
|
539
|
+
// saved query — actions.find may seed from the selection and overwrite it.
|
|
540
|
+
setTimeout(function() {
|
|
541
|
+
try {
|
|
542
|
+
editor.trigger('', 'actions.find', null);
|
|
543
|
+
setTimeout(function() {
|
|
544
|
+
try {
|
|
545
|
+
var _fc0 = editor.getContribution('editor.contrib.findController');
|
|
546
|
+
if (_fc0 && _fc0.getState) {
|
|
547
|
+
_fc0.getState().change({
|
|
548
|
+
searchString: _savedFind.searchString,
|
|
549
|
+
isRegex: !!_savedFind.isRegex,
|
|
550
|
+
matchCase: !!_savedFind.matchCase,
|
|
551
|
+
wholeWord: !!_savedFind.wholeWord
|
|
552
|
+
}, false);
|
|
553
|
+
}
|
|
554
|
+
} catch (e) {}
|
|
555
|
+
}, 0);
|
|
556
|
+
} catch (e) {}
|
|
557
|
+
}, 0);
|
|
558
|
+
} else {
|
|
559
|
+
// Widget was closed — just seed the state silently so Ctrl+F pre-fills it.
|
|
560
|
+
try {
|
|
561
|
+
var _fc0 = editor.getContribution('editor.contrib.findController');
|
|
562
|
+
if (_fc0 && _fc0.getState) {
|
|
563
|
+
_fc0.getState().change({
|
|
564
|
+
searchString: _savedFind.searchString,
|
|
565
|
+
isRegex: !!_savedFind.isRegex,
|
|
566
|
+
matchCase: !!_savedFind.matchCase,
|
|
567
|
+
wholeWord: !!_savedFind.wholeWord
|
|
568
|
+
}, false);
|
|
569
|
+
}
|
|
570
|
+
} catch (e) {}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
517
574
|
monacoRef.current = editor;
|
|
518
575
|
window.__mbeditorActiveEditor = editor;
|
|
519
576
|
setEditorReady(true);
|
|
@@ -644,6 +701,26 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
644
701
|
_me.aviBase = aviBaseRef.current;
|
|
645
702
|
_me.aviMax = aviMaxRef.current;
|
|
646
703
|
}
|
|
704
|
+
// Save the current find widget state so it can be restored on next tab switch.
|
|
705
|
+
// Only overwrite the global state when there is an actual search string — this
|
|
706
|
+
// prevents a blank/fresh editor from clobbering the shared query.
|
|
707
|
+
if (editorPrefs.persistFindState !== false) {
|
|
708
|
+
try {
|
|
709
|
+
var _fc = editor.getContribution('editor.contrib.findController');
|
|
710
|
+
if (_fc && _fc.getState) {
|
|
711
|
+
var _fs = _fc.getState();
|
|
712
|
+
if (_fs.searchString) {
|
|
713
|
+
window.__mbeditorFindState = {
|
|
714
|
+
searchString: _fs.searchString,
|
|
715
|
+
isRegex: _fs.isRegex,
|
|
716
|
+
matchCase: _fs.matchCase,
|
|
717
|
+
wholeWord: _fs.wholeWord,
|
|
718
|
+
isRevealed: _fs.isRevealed
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (e) {}
|
|
723
|
+
}
|
|
647
724
|
if (window.__mbeditorActiveEditor === editor) {
|
|
648
725
|
window.__mbeditorActiveEditor = null;
|
|
649
726
|
}
|
|
@@ -1172,6 +1249,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1172
1249
|
var ext = parts.length > 1 ? parts.pop().toLowerCase() : '';
|
|
1173
1250
|
var isImage = tab.isImage || IMAGE_EXTENSIONS.includes(ext);
|
|
1174
1251
|
var isMarkdown = ['md', 'markdown'].includes(ext);
|
|
1252
|
+
var fileBaseName = (tab.path || '').split('/').pop().toLowerCase();
|
|
1253
|
+
var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' ||
|
|
1254
|
+
fileBaseName === 'gemfile' || fileBaseName === 'gemfile.lock' || fileBaseName === 'rakefile';
|
|
1175
1255
|
|
|
1176
1256
|
useEffect(function () {
|
|
1177
1257
|
if (isMarkdown && window.marked) {
|
|
@@ -1204,6 +1284,36 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1204
1284
|
}
|
|
1205
1285
|
}, [markdownContent, isMarkdown]);
|
|
1206
1286
|
|
|
1287
|
+
// Click-outside handler to close the methods dropdown
|
|
1288
|
+
useEffect(function() {
|
|
1289
|
+
if (!methodsOpen) return;
|
|
1290
|
+
function handleClickOutside(e) {
|
|
1291
|
+
var btn = methodsBtnRef.current;
|
|
1292
|
+
// Close if click is not on the button (the dropdown uses onMouseDown with preventDefault)
|
|
1293
|
+
if (btn && !btn.contains(e.target)) {
|
|
1294
|
+
setMethodsOpen(false);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
1298
|
+
return function() { document.removeEventListener('mousedown', handleClickOutside); };
|
|
1299
|
+
}, [methodsOpen]);
|
|
1300
|
+
|
|
1301
|
+
// Parse all method definitions from the current Monaco model
|
|
1302
|
+
function parseRubyMethods(model) {
|
|
1303
|
+
var methods = [];
|
|
1304
|
+
var lineCount = model.getLineCount();
|
|
1305
|
+
var DEF_RE = /^\s*def\s+(self\.)?([a-zA-Z_][a-zA-Z0-9_?!=]*)/;
|
|
1306
|
+
for (var i = 1; i <= lineCount; i++) {
|
|
1307
|
+
var line = model.getLineContent(i);
|
|
1308
|
+
var m = DEF_RE.exec(line);
|
|
1309
|
+
if (m) {
|
|
1310
|
+
var selfPrefix = m[1] ? 'self.' : '';
|
|
1311
|
+
methods.push({ line: i, name: selfPrefix + m[2] });
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return methods;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1207
1317
|
if (tab.fileNotFound) {
|
|
1208
1318
|
return React.createElement(
|
|
1209
1319
|
'div',
|
|
@@ -1282,6 +1392,28 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1282
1392
|
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1283
1393
|
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
|
|
1284
1394
|
),
|
|
1395
|
+
isRubyFile && React.createElement(
|
|
1396
|
+
'button',
|
|
1397
|
+
{
|
|
1398
|
+
ref: methodsBtnRef,
|
|
1399
|
+
className: 'ide-icon-btn' + (methodsOpen ? ' active' : ''),
|
|
1400
|
+
onClick: function() {
|
|
1401
|
+
var nextOpen = !methodsOpen;
|
|
1402
|
+
if (nextOpen) {
|
|
1403
|
+
var model = monacoRef.current && monacoRef.current.getModel();
|
|
1404
|
+
setMethodsList(model ? parseRubyMethods(model) : []);
|
|
1405
|
+
if (methodsBtnRef.current) {
|
|
1406
|
+
var rect = methodsBtnRef.current.getBoundingClientRect();
|
|
1407
|
+
setMethodsDropdownPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
setMethodsOpen(nextOpen);
|
|
1411
|
+
},
|
|
1412
|
+
title: 'Jump to Method'
|
|
1413
|
+
},
|
|
1414
|
+
React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1415
|
+
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'Methods')
|
|
1416
|
+
),
|
|
1285
1417
|
gitAvailable && React.createElement(
|
|
1286
1418
|
'button',
|
|
1287
1419
|
{
|
|
@@ -1306,6 +1438,35 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1306
1438
|
)
|
|
1307
1439
|
),
|
|
1308
1440
|
React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
|
|
1441
|
+
methodsOpen && methodsDropdownPos && React.createElement(
|
|
1442
|
+
'div',
|
|
1443
|
+
{
|
|
1444
|
+
className: 'ide-methods-dropdown',
|
|
1445
|
+
style: { position: 'fixed', top: methodsDropdownPos.top + 'px', right: methodsDropdownPos.right + 'px', left: 'auto', zIndex: 9900 }
|
|
1446
|
+
},
|
|
1447
|
+
methodsList.length === 0
|
|
1448
|
+
? React.createElement('div', { className: 'ide-methods-dropdown-empty' }, 'No methods found')
|
|
1449
|
+
: methodsList.map(function(m) {
|
|
1450
|
+
return React.createElement(
|
|
1451
|
+
'div',
|
|
1452
|
+
{
|
|
1453
|
+
key: m.line,
|
|
1454
|
+
className: 'ide-methods-dropdown-item',
|
|
1455
|
+
onMouseDown: function(e) {
|
|
1456
|
+
e.preventDefault();
|
|
1457
|
+
setMethodsOpen(false);
|
|
1458
|
+
if (monacoRef.current) {
|
|
1459
|
+
monacoRef.current.revealLineInCenter(m.line);
|
|
1460
|
+
monacoRef.current.setPosition({ lineNumber: m.line, column: 1 });
|
|
1461
|
+
monacoRef.current.focus();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
React.createElement('span', { className: 'ide-methods-dropdown-line' }, m.line),
|
|
1466
|
+
m.name
|
|
1467
|
+
);
|
|
1468
|
+
})
|
|
1469
|
+
),
|
|
1309
1470
|
React.createElement('div', { ref: vimStatusRef, className: 'vim-statusbar', style: { display: editorPrefs.vimMode ? 'flex' : 'none', height: '22px', alignItems: 'center', padding: '0 10px', fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", fontSize: '12px', background: 'var(--ide-statusbar-bg, #1e1e2e)', color: 'var(--ide-statusbar-fg, #9cdcfe)', borderTop: '1px solid var(--ide-border, #3e3e3e)', flexShrink: 0, userSelect: 'none', letterSpacing: '0.02em' } })
|
|
1310
1471
|
);
|
|
1311
1472
|
};
|