mbeditor 0.1.2 → 0.1.4
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/README.md +3 -0
- data/app/assets/javascripts/mbeditor/application.js +5 -5
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +114 -32
- data/app/assets/javascripts/mbeditor/components/FileTree.js +17 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +32 -5
- data/app/assets/javascripts/mbeditor/editor_store.js +16 -1
- data/app/assets/javascripts/mbeditor/search_service.js +20 -14
- data/app/assets/stylesheets/mbeditor/application.css +21 -0
- data/app/assets/stylesheets/mbeditor/editor.css +7 -0
- data/app/controllers/mbeditor/application_controller.rb +49 -0
- data/app/controllers/mbeditor/editors_controller.rb +3 -53
- data/app/controllers/mbeditor/git_controller.rb +4 -11
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/engine.rb +6 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +30 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45f561cf7e36141944025bc54ce679c4492a7c7315902532e350eab44b138361
|
|
4
|
+
data.tar.gz: 7612a0802d9c8e65d0539b621362b0adc3f09866bf131b14b707efafcd35e752
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c896888e924acc1d11e38a0efcdf6f7caec6013eb02426e4d4a0ee847accb3eb5872e5a40e1d4ca7551892352d9fce72ae4667a9585c8265ec7ffc7927e85341
|
|
7
|
+
data.tar.gz: d8cbae4ecfd6b3ec04fd5fde655a52cd176830ff38d02c89bd76e7b09372be63b0f227261ec3dcfda2cd79edbe9811a0a2bfddff58a51b2c5d1d36ac199fc3c5
|
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# mbeditor
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/mbeditor)
|
|
4
|
+
[](https://github.com/ojnoonan/mbeditor/actions/workflows/test.yml)
|
|
5
|
+
|
|
3
6
|
Mbeditor (Mini Browser Editor) is a mountable Rails engine that adds a browser-based editor UI to a Rails app.
|
|
4
7
|
|
|
5
8
|
## Features
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
//= require mbeditor/editor_store
|
|
2
|
+
//= require mbeditor/file_service
|
|
3
|
+
//= require mbeditor/git_service
|
|
4
|
+
//= require mbeditor/search_service
|
|
5
|
+
//= require mbeditor/tab_manager
|
|
1
6
|
//= require mbeditor/editor_plugins
|
|
2
7
|
//= require mbeditor/components/CollapsibleSection
|
|
3
8
|
//= require mbeditor/components/ShortcutHelp
|
|
@@ -12,8 +17,3 @@
|
|
|
12
17
|
//= require mbeditor/components/QuickOpenDialog
|
|
13
18
|
//= require mbeditor/components/TabBar
|
|
14
19
|
//= require mbeditor/components/MbeditorApp
|
|
15
|
-
//= require mbeditor/editor_store
|
|
16
|
-
//= require mbeditor/file_service
|
|
17
|
-
//= require mbeditor/git_service
|
|
18
|
-
//= require mbeditor/search_service
|
|
19
|
-
//= require mbeditor/tab_manager
|
|
@@ -40,6 +40,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
40
40
|
var setIsBlameLoading = _useState8[1];
|
|
41
41
|
|
|
42
42
|
var blameDecorationsRef = useRef([]);
|
|
43
|
+
var blameZoneIdsRef = useRef([]);
|
|
44
|
+
|
|
45
|
+
var clearBlameZones = function clearBlameZones(editor) {
|
|
46
|
+
if (!editor) return;
|
|
47
|
+
if (blameZoneIdsRef.current.length === 0) return;
|
|
48
|
+
|
|
49
|
+
editor.changeViewZones(function(accessor) {
|
|
50
|
+
blameZoneIdsRef.current.forEach(function(zoneId) {
|
|
51
|
+
accessor.removeZone(zoneId);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
blameZoneIdsRef.current = [];
|
|
55
|
+
};
|
|
43
56
|
|
|
44
57
|
var findTabByPath = function findTabByPath(path) {
|
|
45
58
|
if (!path) return null;
|
|
@@ -143,6 +156,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
143
156
|
});
|
|
144
157
|
|
|
145
158
|
return function () {
|
|
159
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
160
|
+
clearBlameZones(editor);
|
|
146
161
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
147
162
|
if (window.__mbeditorActiveEditor === editor) {
|
|
148
163
|
window.__mbeditorActiveEditor = null;
|
|
@@ -224,14 +239,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
224
239
|
// Reset blame state when file path changes
|
|
225
240
|
useEffect(function () {
|
|
226
241
|
setBlameData(null);
|
|
227
|
-
setIsBlameVisible(false);
|
|
228
242
|
setIsBlameLoading(false);
|
|
243
|
+
|
|
244
|
+
// Clear stale blame render when switching files.
|
|
245
|
+
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
246
|
+
clearBlameZones(monacoRef.current);
|
|
247
|
+
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
248
|
+
}
|
|
229
249
|
}, [tab.path]);
|
|
230
250
|
|
|
231
251
|
// Handle Blame data fetching
|
|
232
252
|
useEffect(function () {
|
|
233
253
|
if (!isBlameVisible) {
|
|
234
254
|
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
255
|
+
clearBlameZones(monacoRef.current);
|
|
235
256
|
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
236
257
|
}
|
|
237
258
|
return;
|
|
@@ -240,7 +261,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
240
261
|
if (!blameData && !isBlameLoading) {
|
|
241
262
|
setIsBlameLoading(true);
|
|
242
263
|
GitService.fetchBlame(tab.path).then(function(data) {
|
|
243
|
-
|
|
264
|
+
var lines = data && Array.isArray(data.lines) ? data.lines : [];
|
|
265
|
+
setBlameData(lines);
|
|
266
|
+
if (lines.length === 0) {
|
|
267
|
+
EditorStore.setStatus('No blame data available for this file', 'warning');
|
|
268
|
+
} else {
|
|
269
|
+
EditorStore.setStatus('Loaded blame for ' + lines.length + ' lines', 'info');
|
|
270
|
+
}
|
|
244
271
|
setIsBlameLoading(false);
|
|
245
272
|
}).catch(function(err) {
|
|
246
273
|
var status = err.response && err.response.status;
|
|
@@ -248,38 +275,89 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
248
275
|
? "File is not tracked by git"
|
|
249
276
|
: "Failed to load blame: " + ((err.response && err.response.data && err.response.data.error) || err.message);
|
|
250
277
|
EditorStore.setStatus(msg, "error");
|
|
278
|
+
setBlameData([]);
|
|
251
279
|
setIsBlameLoading(false);
|
|
252
|
-
setIsBlameVisible(false);
|
|
253
280
|
});
|
|
254
281
|
}
|
|
255
282
|
}, [isBlameVisible, tab.path, blameData, isBlameLoading]);
|
|
256
283
|
|
|
257
|
-
// Render Blame
|
|
284
|
+
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
258
285
|
useEffect(function () {
|
|
259
286
|
if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
|
|
287
|
+
|
|
260
288
|
var editor = monacoRef.current;
|
|
261
|
-
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
289
|
+
var model = editor.getModel();
|
|
290
|
+
var lineCount = model ? model.getLineCount() : 0;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// Clear previous render before rebuilding.
|
|
294
|
+
clearBlameZones(editor);
|
|
295
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
296
|
+
|
|
297
|
+
var normalized = blameData.map(function(lineData) {
|
|
298
|
+
var ln = Number(lineData && lineData.line);
|
|
299
|
+
if (!model || !ln || ln < 1 || ln > lineCount) return null;
|
|
300
|
+
|
|
301
|
+
var sha = lineData && lineData.sha || '';
|
|
302
|
+
var author = lineData && lineData.author || 'Unknown';
|
|
303
|
+
var summary = lineData && lineData.summary || 'No commit message';
|
|
304
|
+
var isUncommitted = sha.substring(0, 8) === '00000000';
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
line: ln,
|
|
308
|
+
sha: sha,
|
|
309
|
+
author: isUncommitted ? 'Not Committed' : author,
|
|
310
|
+
summary: summary,
|
|
311
|
+
isUncommitted: isUncommitted
|
|
312
|
+
};
|
|
313
|
+
}).filter(Boolean);
|
|
314
|
+
|
|
315
|
+
normalized.sort(function(a, b) { return a.line - b.line; });
|
|
316
|
+
|
|
317
|
+
var blocks = [];
|
|
318
|
+
normalized.forEach(function(item) {
|
|
319
|
+
var current = blocks.length > 0 ? blocks[blocks.length - 1] : null;
|
|
320
|
+
if (!current || current.sha !== item.sha || item.line !== current.endLine + 1) {
|
|
321
|
+
blocks.push({
|
|
322
|
+
sha: item.sha,
|
|
323
|
+
author: item.author,
|
|
324
|
+
summary: item.summary,
|
|
325
|
+
isUncommitted: item.isUncommitted,
|
|
326
|
+
startLine: item.line,
|
|
327
|
+
endLine: item.line
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
278
330
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
331
|
+
current.endLine = item.line;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
var zoneIds = [];
|
|
335
|
+
editor.changeViewZones(function(accessor) {
|
|
336
|
+
blocks.forEach(function(block, idx) {
|
|
337
|
+
var header = document.createElement('div');
|
|
338
|
+
header.className = block.isUncommitted
|
|
339
|
+
? 'ide-blame-block-header ide-blame-block-header-uncommitted'
|
|
340
|
+
: 'ide-blame-block-header';
|
|
341
|
+
header.textContent = block.author + ' - ' + block.summary;
|
|
342
|
+
|
|
343
|
+
var zoneId = accessor.addZone({
|
|
344
|
+
afterLineNumber: block.startLine > 1 ? block.startLine - 1 : 0,
|
|
345
|
+
heightInLines: 1,
|
|
346
|
+
domNode: header,
|
|
347
|
+
suppressMouseDown: true
|
|
348
|
+
});
|
|
349
|
+
zoneIds.push(zoneId);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
blameZoneIdsRef.current = zoneIds;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
var message = err && err.message ? err.message : 'Unknown decoration error';
|
|
355
|
+
EditorStore.setStatus('Failed to render blame annotations: ' + message, 'error');
|
|
356
|
+
clearBlameZones(editor);
|
|
357
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Include tab.content so blame re-renders once async file contents finish loading.
|
|
283
361
|
}, [blameData, isBlameVisible, tab.id, tab.content]);
|
|
284
362
|
|
|
285
363
|
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
@@ -337,20 +415,24 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
337
415
|
return React.createElement('div', { className: 'markdown-preview markdown-preview-full', dangerouslySetInnerHTML: { __html: markup } });
|
|
338
416
|
}
|
|
339
417
|
|
|
418
|
+
// Always render the same wrapper structure so the editorRef div is never
|
|
419
|
+
// unmounted when gitAvailable changes (e.g. loaded async after workspace
|
|
420
|
+
// call returns). The toolbar is conditionally included inside the wrapper.
|
|
340
421
|
return React.createElement(
|
|
341
422
|
'div',
|
|
342
423
|
{ className: 'ide-editor-wrapper', style: { display: 'flex', flexDirection: 'column', height: '100%' } },
|
|
343
|
-
React.createElement(
|
|
424
|
+
gitAvailable && React.createElement(
|
|
344
425
|
'div',
|
|
345
426
|
{ className: 'ide-editor-toolbar', style: { display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', background: '#252526', borderBottom: '1px solid #3c3c3c' } },
|
|
346
427
|
React.createElement(
|
|
347
428
|
'button',
|
|
348
|
-
{
|
|
349
|
-
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
429
|
+
{
|
|
430
|
+
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
431
|
+
onClick: function() {
|
|
432
|
+
setIsBlameVisible(function(prev) { return !prev; });
|
|
433
|
+
},
|
|
434
|
+
title: 'Toggle Git Blame',
|
|
435
|
+
style: { fontSize: '12px', padding: '2px 6px', opacity: isBlameVisible ? 1 : 0.6, background: isBlameVisible ? 'rgba(255,255,255,0.1)' : 'transparent', border: 'none', color: '#ccc', cursor: 'pointer', borderRadius: '3px' }
|
|
354
436
|
},
|
|
355
437
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: '6px' } }),
|
|
356
438
|
isBlameLoading ? 'Loading...' : 'Blame'
|
|
@@ -300,5 +300,21 @@ var FileTree = function FileTree(_ref) {
|
|
|
300
300
|
);
|
|
301
301
|
};
|
|
302
302
|
|
|
303
|
+
// Wrap FileTree in React.memo with a custom comparator that only checks
|
|
304
|
+
// the data props that affect what's rendered. Function prop references
|
|
305
|
+
// (event handlers) are re-created on every parent render but do not
|
|
306
|
+
// change the visual output, so we intentionally ignore them here.
|
|
307
|
+
// This prevents O(n) tree traversal on every MbeditorApp re-render
|
|
308
|
+
// caused by unrelated state changes (status messages, git polls, etc.).
|
|
309
|
+
var FileTreeMemo = React.memo(FileTree, function(prev, next) {
|
|
310
|
+
return prev.items === next.items &&
|
|
311
|
+
prev.activePath === next.activePath &&
|
|
312
|
+
prev.selectedPath === next.selectedPath &&
|
|
313
|
+
prev.gitFiles === next.gitFiles &&
|
|
314
|
+
prev.expandedDirs === next.expandedDirs &&
|
|
315
|
+
prev.pendingCreate === next.pendingCreate &&
|
|
316
|
+
prev.pendingRename === next.pendingRename;
|
|
317
|
+
});
|
|
318
|
+
|
|
303
319
|
// Expose globally for sprockets require
|
|
304
|
-
window.FileTree =
|
|
320
|
+
window.FileTree = FileTreeMemo;
|
|
@@ -197,11 +197,15 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
197
197
|
var _useState182 = _slicedToArray(_useState18, 2);
|
|
198
198
|
var showGitPanel = _useState182[0];
|
|
199
199
|
var setShowGitPanel = _useState182[1];
|
|
200
|
+
var showGitPanelRef = useRef(showGitPanel);
|
|
201
|
+
showGitPanelRef.current = showGitPanel;
|
|
200
202
|
|
|
201
203
|
var _useState18g = useState(320);
|
|
202
204
|
var _useState18g2 = _slicedToArray(_useState18g, 2);
|
|
203
205
|
var gitPanelWidth = _useState18g2[0];
|
|
204
206
|
var setGitPanelWidth = _useState18g2[1];
|
|
207
|
+
var gitPanelWidthRef = useRef(gitPanelWidth);
|
|
208
|
+
gitPanelWidthRef.current = gitPanelWidth;
|
|
205
209
|
|
|
206
210
|
var _useState18h = useState(false);
|
|
207
211
|
|
|
@@ -567,7 +571,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
567
571
|
if (!body) return;
|
|
568
572
|
|
|
569
573
|
var rect = body.getBoundingClientRect();
|
|
570
|
-
var reservedRight = EDITOR_MIN_WIDTH + (
|
|
574
|
+
var reservedRight = EDITOR_MIN_WIDTH + (showGitPanelRef.current ? gitPanelWidthRef.current : 0);
|
|
571
575
|
var maxSidebarWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, rect.width - reservedRight));
|
|
572
576
|
var nextWidth = clientX - rect.left;
|
|
573
577
|
setSidebarWidth(clamp(nextWidth, SIDEBAR_MIN_WIDTH, maxSidebarWidth));
|
|
@@ -614,23 +618,41 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
614
618
|
};
|
|
615
619
|
}, []);
|
|
616
620
|
|
|
617
|
-
// Heartbeat — poll
|
|
621
|
+
// Heartbeat — adaptive poll: 30s when connected, 5s when trying to reconnect.
|
|
622
|
+
// Skipped entirely while the tab is hidden (Page Visibility API).
|
|
618
623
|
useEffect(function () {
|
|
619
624
|
var wasOnline = true;
|
|
620
|
-
var
|
|
625
|
+
var timeoutId = null;
|
|
626
|
+
|
|
627
|
+
function schedule() {
|
|
628
|
+
var delay = wasOnline ? 30000 : 5000;
|
|
629
|
+
timeoutId = setTimeout(tick, delay);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function tick() {
|
|
633
|
+
if (document.hidden) {
|
|
634
|
+
// Tab is backgrounded — skip this cycle and reschedule at the normal
|
|
635
|
+
// online interval so we resume quickly once the tab becomes visible again.
|
|
636
|
+
schedule();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
621
639
|
FileService.ping().then(function () {
|
|
622
640
|
if (!wasOnline) {
|
|
623
641
|
wasOnline = true;
|
|
624
642
|
setServerOnline(true);
|
|
625
643
|
}
|
|
644
|
+
schedule();
|
|
626
645
|
}).catch(function () {
|
|
627
646
|
if (wasOnline) {
|
|
628
647
|
wasOnline = false;
|
|
629
648
|
setServerOnline(false);
|
|
630
649
|
}
|
|
650
|
+
schedule();
|
|
631
651
|
});
|
|
632
|
-
}
|
|
633
|
-
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
schedule();
|
|
655
|
+
return function () { clearTimeout(timeoutId); };
|
|
634
656
|
}, []);
|
|
635
657
|
|
|
636
658
|
var handleSelectFile = function handleSelectFile(path, name, line) {
|
|
@@ -2103,6 +2125,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2103
2125
|
"div",
|
|
2104
2126
|
{ className: "statusbar-msg " + state.statusMessage.kind },
|
|
2105
2127
|
state.statusMessage.text
|
|
2128
|
+
),
|
|
2129
|
+
React.createElement(
|
|
2130
|
+
"div",
|
|
2131
|
+
{ className: "statusbar-version" },
|
|
2132
|
+
"v" + (document.body.dataset.mbeditorVersion || "")
|
|
2106
2133
|
)
|
|
2107
2134
|
),
|
|
2108
2135
|
|
|
@@ -36,6 +36,21 @@ var EditorStore = (function () {
|
|
|
36
36
|
emit();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Subscribe to changes in a specific subset of state keys.
|
|
40
|
+
// The listener is only called when at least one of the watched keys changes
|
|
41
|
+
// by reference (===), preventing unnecessary re-renders for unrelated updates.
|
|
42
|
+
function subscribeToSlice(keys, fn) {
|
|
43
|
+
var prev = {};
|
|
44
|
+
keys.forEach(function(k) { prev[k] = _state[k]; });
|
|
45
|
+
return subscribe(function(newState) {
|
|
46
|
+
var changed = keys.some(function(k) { return newState[k] !== prev[k]; });
|
|
47
|
+
if (changed) {
|
|
48
|
+
keys.forEach(function(k) { prev[k] = newState[k]; });
|
|
49
|
+
fn(newState);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function setStatus(text, kind) {
|
|
40
55
|
kind = kind || "info";
|
|
41
56
|
setState({ statusMessage: { text: text, kind: kind } });
|
|
@@ -49,5 +64,5 @@ var EditorStore = (function () {
|
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
return { getState: getState, subscribe: subscribe, setState: setState, setStatus: setStatus };
|
|
67
|
+
return { getState: getState, subscribe: subscribe, subscribeToSlice: subscribeToSlice, setState: setState, setStatus: setStatus };
|
|
53
68
|
})();
|
|
@@ -9,22 +9,28 @@ var SearchService = (function () {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
function buildIndex(treeData) {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Capture the tree data immediately so a subsequent refresh doesn't
|
|
13
|
+
// clobber us before the idle callback fires.
|
|
14
|
+
var snapshot = treeData;
|
|
15
|
+
var schedule = window.requestIdleCallback || function(cb) { setTimeout(cb, 50); };
|
|
16
|
+
schedule(function() {
|
|
17
|
+
var docs = [];
|
|
18
|
+
var idCounter = 1;
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
function traverse(nodes) {
|
|
21
|
+
nodes.forEach(function(n) {
|
|
22
|
+
if (n.type === 'file') {
|
|
23
|
+
docs.push({ id: idCounter++, path: n.path, name: n.name });
|
|
24
|
+
} else if (n.children) {
|
|
25
|
+
traverse(n.children);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
traverse(snapshot);
|
|
31
|
+
_miniSearch.removeAll();
|
|
32
|
+
_miniSearch.addAll(docs);
|
|
33
|
+
});
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function searchFiles(query) {
|
|
@@ -461,6 +461,27 @@
|
|
|
461
461
|
user-select: none;
|
|
462
462
|
opacity: 0.7;
|
|
463
463
|
}
|
|
464
|
+
.ide-blame-widget {
|
|
465
|
+
pointer-events: none;
|
|
466
|
+
white-space: pre;
|
|
467
|
+
margin-left: 10px;
|
|
468
|
+
}
|
|
469
|
+
.ide-blame-block-header {
|
|
470
|
+
font-size: 11px;
|
|
471
|
+
font-style: italic;
|
|
472
|
+
font-family: var(--font-mono);
|
|
473
|
+
color: #9aa0a6;
|
|
474
|
+
opacity: 0.85;
|
|
475
|
+
white-space: nowrap;
|
|
476
|
+
overflow: hidden;
|
|
477
|
+
text-overflow: ellipsis;
|
|
478
|
+
border-top: 1px dashed rgba(255, 255, 255, 0.08);
|
|
479
|
+
padding: 2px 10px;
|
|
480
|
+
pointer-events: none;
|
|
481
|
+
}
|
|
482
|
+
.ide-blame-block-header-uncommitted {
|
|
483
|
+
color: #4CAF50;
|
|
484
|
+
}
|
|
464
485
|
|
|
465
486
|
/* Code Review Panel & Redmine */
|
|
466
487
|
.ide-code-review {
|
|
@@ -641,6 +641,13 @@ html, body, #mbeditor-root {
|
|
|
641
641
|
.statusbar-msg.error { color: #f48771; }
|
|
642
642
|
.statusbar-msg.success { color: #89d185; }
|
|
643
643
|
|
|
644
|
+
.statusbar-version {
|
|
645
|
+
color: rgba(255,255,255,0.45);
|
|
646
|
+
font-size: 10px;
|
|
647
|
+
white-space: nowrap;
|
|
648
|
+
padding-left: 8px;
|
|
649
|
+
}
|
|
650
|
+
|
|
644
651
|
/* ── Search Panel ─────────────────────────────────────────── */
|
|
645
652
|
.search-panel {
|
|
646
653
|
padding: 8px;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
1
6
|
module Mbeditor
|
|
2
7
|
class ApplicationController < ActionController::Base
|
|
3
8
|
private
|
|
@@ -6,5 +11,49 @@ module Mbeditor
|
|
|
6
11
|
allowed = Array(Mbeditor.configuration.allowed_environments).map(&:to_sym)
|
|
7
12
|
render plain: 'Not found', status: :not_found unless allowed.include?(Rails.env.to_sym)
|
|
8
13
|
end
|
|
14
|
+
|
|
15
|
+
def workspace_root
|
|
16
|
+
configured_root = Mbeditor.configuration.workspace_root
|
|
17
|
+
if configured_root.present?
|
|
18
|
+
Pathname.new(configured_root.to_s)
|
|
19
|
+
else
|
|
20
|
+
self.class.instance_variable_get(:@workspace_root_cache) ||
|
|
21
|
+
self.class.instance_variable_set(:@workspace_root_cache, begin
|
|
22
|
+
rails_root = Rails.root.to_s
|
|
23
|
+
out, status = Open3.capture2("git", "-C", rails_root, "rev-parse", "--show-toplevel")
|
|
24
|
+
Pathname.new(status.success? && out.strip.present? ? out.strip : rails_root)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
Rails.root
|
|
27
|
+
end)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Expand path and confirm it's inside workspace_root.
|
|
32
|
+
# For existing paths we also resolve symlinks so that a symlink inside the
|
|
33
|
+
# workspace that points outside cannot be used to escape the sandbox.
|
|
34
|
+
def resolve_path(raw)
|
|
35
|
+
return nil if raw.blank?
|
|
36
|
+
|
|
37
|
+
root = workspace_root.to_s
|
|
38
|
+
full = File.expand_path(raw.to_s, root)
|
|
39
|
+
return nil unless full.start_with?("#{root}/") || full == root
|
|
40
|
+
|
|
41
|
+
if File.exist?(full)
|
|
42
|
+
real_root = File.realpath(root)
|
|
43
|
+
real = File.realpath(full)
|
|
44
|
+
return nil unless real.start_with?("#{real_root}/") || real == real_root
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
full
|
|
48
|
+
rescue Errno::EACCES
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def relative_path(full)
|
|
53
|
+
root = workspace_root.to_s
|
|
54
|
+
return "" if full == root
|
|
55
|
+
|
|
56
|
+
full.delete_prefix(root + "/")
|
|
57
|
+
end
|
|
9
58
|
end
|
|
10
59
|
end
|
|
@@ -12,12 +12,6 @@ module Mbeditor
|
|
|
12
12
|
before_action :ensure_allowed_environment!
|
|
13
13
|
before_action :verify_mbeditor_client, unless: -> { request.get? || request.head? }
|
|
14
14
|
|
|
15
|
-
ALLOWED_EXTENSIONS = %w[
|
|
16
|
-
rb js jsx ts tsx css scss sass html erb haml slim
|
|
17
|
-
json yaml yml md txt gemspec gemfile rakefile
|
|
18
|
-
gitignore env sh bash zsh conf config toml
|
|
19
|
-
].freeze
|
|
20
|
-
|
|
21
15
|
IMAGE_EXTENSIONS = %w[png jpg jpeg gif svg ico webp bmp avif].freeze
|
|
22
16
|
MAX_OPEN_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
|
23
17
|
RG_AVAILABLE = system("which rg > /dev/null 2>&1")
|
|
@@ -29,8 +23,9 @@ module Mbeditor
|
|
|
29
23
|
end
|
|
30
24
|
|
|
31
25
|
# GET /mbeditor/ping — heartbeat for the frontend connectivity check
|
|
26
|
+
# Silence the log line so development consoles are not spammed.
|
|
32
27
|
def ping
|
|
33
|
-
render json: { ok: true }
|
|
28
|
+
Rails.logger.silence { render json: { ok: true } }
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
# GET /mbeditor/workspace — metadata about current workspace root
|
|
@@ -171,11 +166,6 @@ module Mbeditor
|
|
|
171
166
|
render json: { error: e.message }, status: :unprocessable_entity
|
|
172
167
|
end
|
|
173
168
|
|
|
174
|
-
# Backward compatibility for stale route/action caches.
|
|
175
|
-
def rename_path
|
|
176
|
-
rename
|
|
177
|
-
end
|
|
178
|
-
|
|
179
169
|
# DELETE /mbeditor/delete — remove file or directory
|
|
180
170
|
def destroy_path
|
|
181
171
|
path = resolve_path(params[:path])
|
|
@@ -194,11 +184,6 @@ module Mbeditor
|
|
|
194
184
|
render json: { error: e.message }, status: :unprocessable_entity
|
|
195
185
|
end
|
|
196
186
|
|
|
197
|
-
# Backward compatibility for stale route/action caches.
|
|
198
|
-
def delete_path
|
|
199
|
-
destroy_path
|
|
200
|
-
end
|
|
201
|
-
|
|
202
187
|
# GET /mbeditor/search?q=...
|
|
203
188
|
def search
|
|
204
189
|
query = params[:q].to_s.strip
|
|
@@ -408,41 +393,6 @@ module Mbeditor
|
|
|
408
393
|
render plain: 'Forbidden', status: :forbidden
|
|
409
394
|
end
|
|
410
395
|
|
|
411
|
-
def workspace_root
|
|
412
|
-
configured_root = Mbeditor.configuration.workspace_root
|
|
413
|
-
if configured_root.present?
|
|
414
|
-
# Explicitly configured — no subprocess required, just wrap it.
|
|
415
|
-
Pathname.new(configured_root.to_s)
|
|
416
|
-
else
|
|
417
|
-
# Auto-detect from git. Cache the result since the git root cannot change
|
|
418
|
-
# within a running process.
|
|
419
|
-
self.class.instance_variable_get(:@workspace_root_cache) ||
|
|
420
|
-
self.class.instance_variable_set(:@workspace_root_cache, begin
|
|
421
|
-
rails_root = Rails.root.to_s
|
|
422
|
-
out, status = Open3.capture2("git", "-C", rails_root, "rev-parse", "--show-toplevel")
|
|
423
|
-
Pathname.new(status.success? && out.strip.present? ? out.strip : rails_root)
|
|
424
|
-
rescue StandardError
|
|
425
|
-
Rails.root
|
|
426
|
-
end)
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
# Expand path and confirm it's inside workspace_root
|
|
431
|
-
def resolve_path(raw)
|
|
432
|
-
return nil if raw.blank?
|
|
433
|
-
|
|
434
|
-
root = workspace_root.to_s
|
|
435
|
-
full = File.expand_path(raw.to_s, root)
|
|
436
|
-
full.start_with?(root + "/") || full == root ? full : nil
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def relative_path(full)
|
|
440
|
-
root = workspace_root.to_s
|
|
441
|
-
return "" if full == root
|
|
442
|
-
|
|
443
|
-
full.delete_prefix(root + "/")
|
|
444
|
-
end
|
|
445
|
-
|
|
446
396
|
def path_blocked_for_operations?(full_path)
|
|
447
397
|
rel = relative_path(full_path)
|
|
448
398
|
return true if rel.blank?
|
|
@@ -453,7 +403,7 @@ module Mbeditor
|
|
|
453
403
|
def build_tree(dir, max_depth: 10, depth: 0)
|
|
454
404
|
return [] if depth >= max_depth
|
|
455
405
|
|
|
456
|
-
entries = Dir.entries(dir).sort.reject { |entry| entry
|
|
406
|
+
entries = Dir.entries(dir).sort.reject { |entry| entry == "." || entry == ".." }
|
|
457
407
|
entries.filter_map do |name|
|
|
458
408
|
full = File.join(dir, name)
|
|
459
409
|
rel = relative_path(full)
|
|
@@ -137,7 +137,7 @@ module Mbeditor
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
render plain: out, content_type: "text/plain"
|
|
140
|
-
rescue StandardError
|
|
140
|
+
rescue StandardError
|
|
141
141
|
render plain: "", content_type: "text/plain"
|
|
142
142
|
end
|
|
143
143
|
|
|
@@ -159,11 +159,6 @@ module Mbeditor
|
|
|
159
159
|
|
|
160
160
|
private
|
|
161
161
|
|
|
162
|
-
def workspace_root
|
|
163
|
-
configured = Mbeditor.configuration.workspace_root
|
|
164
|
-
configured.present? ? configured.to_s : Rails.root.to_s
|
|
165
|
-
end
|
|
166
|
-
|
|
167
162
|
# Require & validate a `file` query param, responding 400/403 on bad input.
|
|
168
163
|
# Returns the relative path string on success, or nil if already responded.
|
|
169
164
|
def require_file_param
|
|
@@ -174,15 +169,13 @@ module Mbeditor
|
|
|
174
169
|
return nil
|
|
175
170
|
end
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
full = File.expand_path(raw, root)
|
|
180
|
-
unless full.start_with?(root + "/") || full == root
|
|
172
|
+
full = resolve_path(raw)
|
|
173
|
+
unless full
|
|
181
174
|
render json: { error: "Forbidden" }, status: :forbidden
|
|
182
175
|
return nil
|
|
183
176
|
end
|
|
184
177
|
|
|
185
|
-
|
|
178
|
+
relative_path(full)
|
|
186
179
|
end
|
|
187
180
|
end
|
|
188
181
|
end
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<script>var require = { paths: { vs: '<%= json_escape("#{base_path}/monaco-editor/vs") %>' } };</script>
|
|
30
30
|
<script src="<%= "#{base_path}/monaco-editor/vs/loader.js" %>"></script>
|
|
31
31
|
</head>
|
|
32
|
-
<body data-rails-root="<%= Rails.root.to_s %>">
|
|
32
|
+
<body data-rails-root="<%= Rails.root.to_s %>" data-mbeditor-version="<%= Mbeditor::VERSION %>">
|
|
33
33
|
<script>
|
|
34
34
|
window.MBEDITOR_BASE_PATH = "<%= json_escape(base_path) %>";
|
|
35
35
|
window.prettierPlugins = {
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
require "mbeditor/rack/silence_ping_request"
|
|
2
|
+
|
|
1
3
|
module Mbeditor
|
|
2
4
|
class Engine < ::Rails::Engine
|
|
3
5
|
isolate_namespace Mbeditor
|
|
4
6
|
|
|
7
|
+
initializer "mbeditor.silence_ping_request" do |app|
|
|
8
|
+
app.middleware.insert_before Rails::Rack::Logger, Mbeditor::Rack::SilencePingRequest
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
initializer "mbeditor.assets.precompile" do |app|
|
|
6
12
|
app.config.assets.precompile += %w[
|
|
7
13
|
mbeditor/application.css
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/logger_silence"
|
|
4
|
+
|
|
5
|
+
module Mbeditor
|
|
6
|
+
module Rack
|
|
7
|
+
# Silence periodic editor heartbeats so development logs stay readable.
|
|
8
|
+
class SilencePingRequest
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
if ping_request?(env)
|
|
15
|
+
Rails.logger.silence { @app.call(env) }
|
|
16
|
+
else
|
|
17
|
+
@app.call(env)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def ping_request?(env)
|
|
24
|
+
env["REQUEST_METHOD"] == "GET" &&
|
|
25
|
+
env["HTTP_X_MBEDITOR_CLIENT"] == "1" &&
|
|
26
|
+
env["PATH_INFO"].to_s.end_with?("/ping")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- lib/mbeditor.rb
|
|
93
93
|
- lib/mbeditor/configuration.rb
|
|
94
94
|
- lib/mbeditor/engine.rb
|
|
95
|
+
- lib/mbeditor/rack/silence_ping_request.rb
|
|
95
96
|
- lib/mbeditor/version.rb
|
|
96
97
|
- mbeditor.gemspec
|
|
97
98
|
- public/min-maps/vs/base/worker/workerMain.js.map
|