mbeditor 0.1.2 → 0.1.5
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 +6 -5
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +126 -36
- data/app/assets/javascripts/mbeditor/components/FileTree.js +17 -18
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +21 -32
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +100 -26
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +50 -18
- data/app/assets/javascripts/mbeditor/editor_store.js +16 -1
- data/app/assets/javascripts/mbeditor/file_icon.js +30 -0
- 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 +105 -5
- data/app/controllers/mbeditor/application_controller.rb +49 -0
- data/app/controllers/mbeditor/editors_controller.rb +4 -54
- 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
- data/public/monaco-editor/vs/basic-languages/shell/shell.js +41 -0
- data/public/monaco-editor/vs/basic-languages/typescript/typescript.js +10 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 414b42474453e80d58cfafbab5db7d5381dd8ebb22ba1e852a10e720eb246d8a
|
|
4
|
+
data.tar.gz: 0fbaffaedc1aed6196589975816fb9b009df7f4a810dcc41e3aa428d8e19ee37
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bab09aab7ae92d7835838d3c4b240915d0c3ff0c3fc32516a69bdad24430246c86c0da3f508946e9c738ca94a0966d7457903ba9095000a4f01643ca298dbe7
|
|
7
|
+
data.tar.gz: 522f1e56c9c280ccf9078f9f960ad1c3e3f1d1f99fafc2e9d80c18d539cd6ba409d18cb596b434037e551403623b74c54c7f2f4dfe8c03c90760eebe8b51862c
|
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,9 @@
|
|
|
1
|
+
//= require mbeditor/editor_store
|
|
2
|
+
//= require mbeditor/file_icon
|
|
3
|
+
//= require mbeditor/file_service
|
|
4
|
+
//= require mbeditor/git_service
|
|
5
|
+
//= require mbeditor/search_service
|
|
6
|
+
//= require mbeditor/tab_manager
|
|
1
7
|
//= require mbeditor/editor_plugins
|
|
2
8
|
//= require mbeditor/components/CollapsibleSection
|
|
3
9
|
//= require mbeditor/components/ShortcutHelp
|
|
@@ -12,8 +18,3 @@
|
|
|
12
18
|
//= require mbeditor/components/QuickOpenDialog
|
|
13
19
|
//= require mbeditor/components/TabBar
|
|
14
20
|
//= 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
|
|
@@ -83,10 +83,12 @@ var DiffViewer = function DiffViewer(_ref) {
|
|
|
83
83
|
|
|
84
84
|
function getLanguageForPath(filePath) {
|
|
85
85
|
if (!filePath) return 'plaintext';
|
|
86
|
+
var fileName = filePath.split('/').pop().toLowerCase();
|
|
87
|
+
if (fileName === 'gemfile' || fileName === 'gemfile.lock' || fileName === 'rakefile') return 'ruby';
|
|
86
88
|
var ext = filePath.split('.').pop().toLowerCase();
|
|
87
89
|
var map = {
|
|
88
90
|
'rb': 'ruby', 'js': 'javascript', 'jsx': 'javascript',
|
|
89
|
-
'ts': '
|
|
91
|
+
'ts': 'typescript', 'tsx': 'typescript',
|
|
90
92
|
'json': 'json', 'yml': 'yaml', 'yaml': 'yaml',
|
|
91
93
|
'css': 'css', 'scss': 'scss', 'html': 'html',
|
|
92
94
|
'xml': 'xml', 'md': 'markdown', 'sh': 'shell'
|
|
@@ -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;
|
|
@@ -62,16 +75,23 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
62
75
|
window.MbeditorEditorPlugins.registerGlobalExtensions(window.monaco);
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
var
|
|
78
|
+
var fileName = tab.path.split('/').pop() || '';
|
|
79
|
+
var parts = fileName.split('.');
|
|
66
80
|
var extension = parts.length > 1 ? parts.pop().toLowerCase() : '';
|
|
67
81
|
var language = 'plaintext';
|
|
68
|
-
switch (
|
|
69
|
-
case '
|
|
82
|
+
switch (fileName.toLowerCase()) {
|
|
83
|
+
case 'gemfile':
|
|
84
|
+
case 'gemfile.lock':
|
|
85
|
+
case 'rakefile':
|
|
70
86
|
language = 'ruby';break;
|
|
87
|
+
default:
|
|
88
|
+
switch (extension) {
|
|
89
|
+
case 'rb':case 'ruby':case 'gemspec':
|
|
90
|
+
language = 'ruby';break;
|
|
71
91
|
case 'js':case 'jsx':
|
|
72
92
|
language = 'javascript';break;
|
|
73
93
|
case 'ts':case 'tsx':
|
|
74
|
-
language = '
|
|
94
|
+
language = 'typescript';break;
|
|
75
95
|
case 'css':case 'scss':case 'sass':
|
|
76
96
|
language = 'css';break;
|
|
77
97
|
case 'html':case 'erb':
|
|
@@ -88,6 +108,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
88
108
|
language = 'shell';break;
|
|
89
109
|
case 'png':case 'jpg':case 'jpeg':case 'gif':case 'svg':case 'ico':case 'webp':case 'bmp':case 'avif':
|
|
90
110
|
language = 'image';break;
|
|
111
|
+
}
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
if (language === 'image') return;
|
|
@@ -143,6 +164,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
143
164
|
});
|
|
144
165
|
|
|
145
166
|
return function () {
|
|
167
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
168
|
+
clearBlameZones(editor);
|
|
146
169
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
147
170
|
if (window.__mbeditorActiveEditor === editor) {
|
|
148
171
|
window.__mbeditorActiveEditor = null;
|
|
@@ -224,14 +247,20 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
224
247
|
// Reset blame state when file path changes
|
|
225
248
|
useEffect(function () {
|
|
226
249
|
setBlameData(null);
|
|
227
|
-
setIsBlameVisible(false);
|
|
228
250
|
setIsBlameLoading(false);
|
|
251
|
+
|
|
252
|
+
// Clear stale blame render when switching files.
|
|
253
|
+
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
254
|
+
clearBlameZones(monacoRef.current);
|
|
255
|
+
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
256
|
+
}
|
|
229
257
|
}, [tab.path]);
|
|
230
258
|
|
|
231
259
|
// Handle Blame data fetching
|
|
232
260
|
useEffect(function () {
|
|
233
261
|
if (!isBlameVisible) {
|
|
234
262
|
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
263
|
+
clearBlameZones(monacoRef.current);
|
|
235
264
|
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
236
265
|
}
|
|
237
266
|
return;
|
|
@@ -240,7 +269,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
240
269
|
if (!blameData && !isBlameLoading) {
|
|
241
270
|
setIsBlameLoading(true);
|
|
242
271
|
GitService.fetchBlame(tab.path).then(function(data) {
|
|
243
|
-
|
|
272
|
+
var lines = data && Array.isArray(data.lines) ? data.lines : [];
|
|
273
|
+
setBlameData(lines);
|
|
274
|
+
if (lines.length === 0) {
|
|
275
|
+
EditorStore.setStatus('No blame data available for this file', 'warning');
|
|
276
|
+
} else {
|
|
277
|
+
EditorStore.setStatus('Loaded blame for ' + lines.length + ' lines', 'info');
|
|
278
|
+
}
|
|
244
279
|
setIsBlameLoading(false);
|
|
245
280
|
}).catch(function(err) {
|
|
246
281
|
var status = err.response && err.response.status;
|
|
@@ -248,38 +283,89 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
248
283
|
? "File is not tracked by git"
|
|
249
284
|
: "Failed to load blame: " + ((err.response && err.response.data && err.response.data.error) || err.message);
|
|
250
285
|
EditorStore.setStatus(msg, "error");
|
|
286
|
+
setBlameData([]);
|
|
251
287
|
setIsBlameLoading(false);
|
|
252
|
-
setIsBlameVisible(false);
|
|
253
288
|
});
|
|
254
289
|
}
|
|
255
290
|
}, [isBlameVisible, tab.path, blameData, isBlameLoading]);
|
|
256
291
|
|
|
257
|
-
// Render Blame
|
|
292
|
+
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
258
293
|
useEffect(function () {
|
|
259
294
|
if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
|
|
295
|
+
|
|
260
296
|
var editor = monacoRef.current;
|
|
261
|
-
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
var model = editor.getModel();
|
|
298
|
+
var lineCount = model ? model.getLineCount() : 0;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Clear previous render before rebuilding.
|
|
302
|
+
clearBlameZones(editor);
|
|
303
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
304
|
+
|
|
305
|
+
var normalized = blameData.map(function(lineData) {
|
|
306
|
+
var ln = Number(lineData && lineData.line);
|
|
307
|
+
if (!model || !ln || ln < 1 || ln > lineCount) return null;
|
|
308
|
+
|
|
309
|
+
var sha = lineData && lineData.sha || '';
|
|
310
|
+
var author = lineData && lineData.author || 'Unknown';
|
|
311
|
+
var summary = lineData && lineData.summary || 'No commit message';
|
|
312
|
+
var isUncommitted = sha.substring(0, 8) === '00000000';
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
line: ln,
|
|
316
|
+
sha: sha,
|
|
317
|
+
author: isUncommitted ? 'Not Committed' : author,
|
|
318
|
+
summary: summary,
|
|
319
|
+
isUncommitted: isUncommitted
|
|
320
|
+
};
|
|
321
|
+
}).filter(Boolean);
|
|
322
|
+
|
|
323
|
+
normalized.sort(function(a, b) { return a.line - b.line; });
|
|
324
|
+
|
|
325
|
+
var blocks = [];
|
|
326
|
+
normalized.forEach(function(item) {
|
|
327
|
+
var current = blocks.length > 0 ? blocks[blocks.length - 1] : null;
|
|
328
|
+
if (!current || current.sha !== item.sha || item.line !== current.endLine + 1) {
|
|
329
|
+
blocks.push({
|
|
330
|
+
sha: item.sha,
|
|
331
|
+
author: item.author,
|
|
332
|
+
summary: item.summary,
|
|
333
|
+
isUncommitted: item.isUncommitted,
|
|
334
|
+
startLine: item.line,
|
|
335
|
+
endLine: item.line
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
278
338
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
339
|
+
current.endLine = item.line;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
var zoneIds = [];
|
|
343
|
+
editor.changeViewZones(function(accessor) {
|
|
344
|
+
blocks.forEach(function(block, idx) {
|
|
345
|
+
var header = document.createElement('div');
|
|
346
|
+
header.className = block.isUncommitted
|
|
347
|
+
? 'ide-blame-block-header ide-blame-block-header-uncommitted'
|
|
348
|
+
: 'ide-blame-block-header';
|
|
349
|
+
header.textContent = block.author + ' - ' + block.summary;
|
|
350
|
+
|
|
351
|
+
var zoneId = accessor.addZone({
|
|
352
|
+
afterLineNumber: block.startLine > 1 ? block.startLine - 1 : 0,
|
|
353
|
+
heightInLines: 1,
|
|
354
|
+
domNode: header,
|
|
355
|
+
suppressMouseDown: true
|
|
356
|
+
});
|
|
357
|
+
zoneIds.push(zoneId);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
blameZoneIdsRef.current = zoneIds;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
var message = err && err.message ? err.message : 'Unknown decoration error';
|
|
363
|
+
EditorStore.setStatus('Failed to render blame annotations: ' + message, 'error');
|
|
364
|
+
clearBlameZones(editor);
|
|
365
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Include tab.content so blame re-renders once async file contents finish loading.
|
|
283
369
|
}, [blameData, isBlameVisible, tab.id, tab.content]);
|
|
284
370
|
|
|
285
371
|
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
@@ -337,20 +423,24 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
337
423
|
return React.createElement('div', { className: 'markdown-preview markdown-preview-full', dangerouslySetInnerHTML: { __html: markup } });
|
|
338
424
|
}
|
|
339
425
|
|
|
426
|
+
// Always render the same wrapper structure so the editorRef div is never
|
|
427
|
+
// unmounted when gitAvailable changes (e.g. loaded async after workspace
|
|
428
|
+
// call returns). The toolbar is conditionally included inside the wrapper.
|
|
340
429
|
return React.createElement(
|
|
341
430
|
'div',
|
|
342
431
|
{ className: 'ide-editor-wrapper', style: { display: 'flex', flexDirection: 'column', height: '100%' } },
|
|
343
|
-
React.createElement(
|
|
432
|
+
gitAvailable && React.createElement(
|
|
344
433
|
'div',
|
|
345
434
|
{ className: 'ide-editor-toolbar', style: { display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', background: '#252526', borderBottom: '1px solid #3c3c3c' } },
|
|
346
435
|
React.createElement(
|
|
347
436
|
'button',
|
|
348
|
-
{
|
|
349
|
-
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
437
|
+
{
|
|
438
|
+
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
439
|
+
onClick: function() {
|
|
440
|
+
setIsBlameVisible(function(prev) { return !prev; });
|
|
441
|
+
},
|
|
442
|
+
title: 'Toggle Git Blame',
|
|
443
|
+
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
444
|
},
|
|
355
445
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: '6px' } }),
|
|
356
446
|
isBlameLoading ? 'Loading...' : 'Blame'
|
|
@@ -109,23 +109,6 @@ var FileTree = function FileTree(_ref) {
|
|
|
109
109
|
return { badge: key || '?', cssKey: key || 'Q', title: titleMap[key] || 'Status' };
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
window.getFileIcon = function (name) {
|
|
113
|
-
var ext = name.split('.').pop().toLowerCase();
|
|
114
|
-
var lName = name.toLowerCase();
|
|
115
|
-
if (lName === 'gemfile' || ext === 'gemspec' || ext === 'lock') return 'fas fa-gem ruby-icon';
|
|
116
|
-
if (ext === 'rb' || ext === 'rake' || lName === 'rakefile') return 'far fa-gem ruby-icon';
|
|
117
|
-
if (ext === 'jsx' || name.endsWith('.js.jsx')) return 'fas fa-atom react-icon';
|
|
118
|
-
if (ext === 'js' || ext === 'mjs' || ext === 'cjs') return 'fa-brands fa-js js-icon';
|
|
119
|
-
if (ext === 'html') return 'fa-brands fa-html5 html-icon';
|
|
120
|
-
if (ext === 'erb') return 'fa-brands fa-html5 erb-icon';
|
|
121
|
-
if (ext === 'css' || ext === 'scss' || ext === 'sass') return 'fa-brands fa-css3-alt css-icon';
|
|
122
|
-
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp', 'bmp', 'avif'].includes(ext)) return 'far fa-file-image image-icon';
|
|
123
|
-
if (ext === 'json') return 'fas fa-code json-icon';
|
|
124
|
-
if (ext === 'md' || ext === 'txt') return 'fas fa-file-alt md-icon';
|
|
125
|
-
if (ext === 'yml' || ext === 'yaml') return 'fas fa-cogs yml-icon';
|
|
126
|
-
return 'far fa-file-code';
|
|
127
|
-
};
|
|
128
|
-
|
|
129
112
|
var handleInlineKeyDown = function handleInlineKeyDown(e) {
|
|
130
113
|
var isRename = !!pendingRename;
|
|
131
114
|
if (e.key === 'Enter') {
|
|
@@ -300,5 +283,21 @@ var FileTree = function FileTree(_ref) {
|
|
|
300
283
|
);
|
|
301
284
|
};
|
|
302
285
|
|
|
286
|
+
// Wrap FileTree in React.memo with a custom comparator that only checks
|
|
287
|
+
// the data props that affect what's rendered. Function prop references
|
|
288
|
+
// (event handlers) are re-created on every parent render but do not
|
|
289
|
+
// change the visual output, so we intentionally ignore them here.
|
|
290
|
+
// This prevents O(n) tree traversal on every MbeditorApp re-render
|
|
291
|
+
// caused by unrelated state changes (status messages, git polls, etc.).
|
|
292
|
+
var FileTreeMemo = React.memo(FileTree, function(prev, next) {
|
|
293
|
+
return prev.items === next.items &&
|
|
294
|
+
prev.activePath === next.activePath &&
|
|
295
|
+
prev.selectedPath === next.selectedPath &&
|
|
296
|
+
prev.gitFiles === next.gitFiles &&
|
|
297
|
+
prev.expandedDirs === next.expandedDirs &&
|
|
298
|
+
prev.pendingCreate === next.pendingCreate &&
|
|
299
|
+
prev.pendingRename === next.pendingRename;
|
|
300
|
+
});
|
|
301
|
+
|
|
303
302
|
// Expose globally for sprockets require
|
|
304
|
-
window.FileTree =
|
|
303
|
+
window.FileTree = FileTreeMemo;
|
|
@@ -16,6 +16,7 @@ var GitPanel = function GitPanel(_ref) {
|
|
|
16
16
|
var _s4 = useState({}); var expandedCommits = _s4[0]; var setExpandedCommits = _s4[1];
|
|
17
17
|
// { [hash]: { loading, files: [{status,path}], error } }
|
|
18
18
|
var _s5 = useState({}); var commitFiles = _s5[0]; var setCommitFiles = _s5[1];
|
|
19
|
+
var _s6 = useState(false); var refreshing = _s6[0]; var setRefreshing = _s6[1];
|
|
19
20
|
|
|
20
21
|
var workingTree = gitInfo && gitInfo.workingTree || [];
|
|
21
22
|
var unpushedFiles = gitInfo && gitInfo.unpushedFiles || [];
|
|
@@ -46,36 +47,7 @@ var GitPanel = function GitPanel(_ref) {
|
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
var fileIcon = function fileIcon(filename) {
|
|
49
|
-
|
|
50
|
-
var iconMap = {
|
|
51
|
-
'rb': { cls: 'fas fa-gem', color: '#cc342d' },
|
|
52
|
-
'gemspec': { cls: 'fas fa-gem', color: '#cc342d' },
|
|
53
|
-
'js': { cls: 'fab fa-js', color: '#f0db4f' },
|
|
54
|
-
'ts': { cls: 'fas fa-code', color: '#3178c6' },
|
|
55
|
-
'jsx': { cls: 'fab fa-js', color: '#61dafb' },
|
|
56
|
-
'tsx': { cls: 'fas fa-code', color: '#61dafb' },
|
|
57
|
-
'html': { cls: 'fab fa-html5', color: '#e34f26' },
|
|
58
|
-
'htm': { cls: 'fab fa-html5', color: '#e34f26' },
|
|
59
|
-
'erb': { cls: 'fab fa-html5', color: '#cc342d' },
|
|
60
|
-
'css': { cls: 'fab fa-css3-alt', color: '#1572b6' },
|
|
61
|
-
'scss': { cls: 'fab fa-sass', color: '#cc6699' },
|
|
62
|
-
'sass': { cls: 'fab fa-sass', color: '#cc6699' },
|
|
63
|
-
'md': { cls: 'fab fa-markdown', color: '#7f8b97' },
|
|
64
|
-
'json': { cls: 'fas fa-code', color: '#ffe082' },
|
|
65
|
-
'yml': { cls: 'fas fa-cog', color: '#888' },
|
|
66
|
-
'yaml': { cls: 'fas fa-cog', color: '#888' },
|
|
67
|
-
'png': { cls: 'fas fa-image', color: '#aaa' },
|
|
68
|
-
'jpg': { cls: 'fas fa-image', color: '#aaa' },
|
|
69
|
-
'jpeg': { cls: 'fas fa-image', color: '#aaa' },
|
|
70
|
-
'gif': { cls: 'fas fa-image', color: '#aaa' },
|
|
71
|
-
'svg': { cls: 'fas fa-image', color: '#aaa' },
|
|
72
|
-
'txt': { cls: 'fas fa-file-alt', color: '#888' },
|
|
73
|
-
'sh': { cls: 'fas fa-terminal', color: '#89d185' },
|
|
74
|
-
'lock': { cls: 'fas fa-lock', color: '#888' },
|
|
75
|
-
'pdf': { cls: 'fas fa-file-pdf', color: '#f48771' },
|
|
76
|
-
};
|
|
77
|
-
var icon = iconMap[ext] || { cls: 'fas fa-file-code', color: '#7f8b97' };
|
|
78
|
-
return React.createElement('i', { className: icon.cls + ' git-file-type-icon', style: { color: icon.color } });
|
|
50
|
+
return React.createElement('i', { className: (window.getFileIcon ? window.getFileIcon(filename) : 'far fa-file-code') + ' git-file-type-icon' });
|
|
79
51
|
};
|
|
80
52
|
|
|
81
53
|
// Renders a file row used in Local Changes and Changes in Branch sections
|
|
@@ -148,6 +120,23 @@ var GitPanel = function GitPanel(_ref) {
|
|
|
148
120
|
}
|
|
149
121
|
};
|
|
150
122
|
|
|
123
|
+
var handleRefresh = function handleRefresh() {
|
|
124
|
+
if (!onRefresh || refreshing) return;
|
|
125
|
+
|
|
126
|
+
var result;
|
|
127
|
+
try {
|
|
128
|
+
setRefreshing(true);
|
|
129
|
+
result = onRefresh();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
setRefreshing(false);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return Promise.resolve(result).finally(function () {
|
|
136
|
+
setRefreshing(false);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
151
140
|
var renderCommit = function renderCommit(commit, idx) {
|
|
152
141
|
var isFirst = idx === 0;
|
|
153
142
|
var isLast = idx === branchCommits.length - 1;
|
|
@@ -305,8 +294,8 @@ var GitPanel = function GitPanel(_ref) {
|
|
|
305
294
|
{ className: 'ide-git-panel-actions' },
|
|
306
295
|
onRefresh && React.createElement(
|
|
307
296
|
'button',
|
|
308
|
-
{ className: 'git-header-btn', onClick:
|
|
309
|
-
React.createElement('i', { className: 'fas fa-sync-alt' })
|
|
297
|
+
{ className: 'git-header-btn', onClick: handleRefresh, title: 'Refresh', disabled: refreshing, 'aria-busy': refreshing },
|
|
298
|
+
React.createElement('i', { className: 'fas fa-sync-alt' + (refreshing ? ' fa-spin' : '') })
|
|
310
299
|
),
|
|
311
300
|
onClose && React.createElement(
|
|
312
301
|
'button',
|
|
@@ -121,6 +121,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
121
121
|
var searchQuery = _useState72[0];
|
|
122
122
|
var setSearchQuery = _useState72[1];
|
|
123
123
|
|
|
124
|
+
var _useState33 = useState(false);
|
|
125
|
+
var _useState332 = _slicedToArray(_useState33, 2);
|
|
126
|
+
var searchLoading = _useState332[0];
|
|
127
|
+
var setSearchLoading = _useState332[1];
|
|
128
|
+
|
|
129
|
+
var searchRequestIdRef = useRef(0);
|
|
130
|
+
|
|
124
131
|
var _useState8 = useState("explorer");
|
|
125
132
|
|
|
126
133
|
var _useState82 = _slicedToArray(_useState8, 2);
|
|
@@ -197,11 +204,15 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
197
204
|
var _useState182 = _slicedToArray(_useState18, 2);
|
|
198
205
|
var showGitPanel = _useState182[0];
|
|
199
206
|
var setShowGitPanel = _useState182[1];
|
|
207
|
+
var showGitPanelRef = useRef(showGitPanel);
|
|
208
|
+
showGitPanelRef.current = showGitPanel;
|
|
200
209
|
|
|
201
210
|
var _useState18g = useState(320);
|
|
202
211
|
var _useState18g2 = _slicedToArray(_useState18g, 2);
|
|
203
212
|
var gitPanelWidth = _useState18g2[0];
|
|
204
213
|
var setGitPanelWidth = _useState18g2[1];
|
|
214
|
+
var gitPanelWidthRef = useRef(gitPanelWidth);
|
|
215
|
+
gitPanelWidthRef.current = gitPanelWidth;
|
|
205
216
|
|
|
206
217
|
var _useState18h = useState(false);
|
|
207
218
|
|
|
@@ -567,7 +578,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
567
578
|
if (!body) return;
|
|
568
579
|
|
|
569
580
|
var rect = body.getBoundingClientRect();
|
|
570
|
-
var reservedRight = EDITOR_MIN_WIDTH + (
|
|
581
|
+
var reservedRight = EDITOR_MIN_WIDTH + (showGitPanelRef.current ? gitPanelWidthRef.current : 0);
|
|
571
582
|
var maxSidebarWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, rect.width - reservedRight));
|
|
572
583
|
var nextWidth = clientX - rect.left;
|
|
573
584
|
setSidebarWidth(clamp(nextWidth, SIDEBAR_MIN_WIDTH, maxSidebarWidth));
|
|
@@ -614,23 +625,41 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
614
625
|
};
|
|
615
626
|
}, []);
|
|
616
627
|
|
|
617
|
-
// Heartbeat — poll
|
|
628
|
+
// Heartbeat — adaptive poll: 30s when connected, 5s when trying to reconnect.
|
|
629
|
+
// Skipped entirely while the tab is hidden (Page Visibility API).
|
|
618
630
|
useEffect(function () {
|
|
619
631
|
var wasOnline = true;
|
|
620
|
-
var
|
|
632
|
+
var timeoutId = null;
|
|
633
|
+
|
|
634
|
+
function schedule() {
|
|
635
|
+
var delay = wasOnline ? 30000 : 5000;
|
|
636
|
+
timeoutId = setTimeout(tick, delay);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function tick() {
|
|
640
|
+
if (document.hidden) {
|
|
641
|
+
// Tab is backgrounded — skip this cycle and reschedule at the normal
|
|
642
|
+
// online interval so we resume quickly once the tab becomes visible again.
|
|
643
|
+
schedule();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
621
646
|
FileService.ping().then(function () {
|
|
622
647
|
if (!wasOnline) {
|
|
623
648
|
wasOnline = true;
|
|
624
649
|
setServerOnline(true);
|
|
625
650
|
}
|
|
651
|
+
schedule();
|
|
626
652
|
}).catch(function () {
|
|
627
653
|
if (wasOnline) {
|
|
628
654
|
wasOnline = false;
|
|
629
655
|
setServerOnline(false);
|
|
630
656
|
}
|
|
657
|
+
schedule();
|
|
631
658
|
});
|
|
632
|
-
}
|
|
633
|
-
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
schedule();
|
|
662
|
+
return function () { clearTimeout(timeoutId); };
|
|
634
663
|
}, []);
|
|
635
664
|
|
|
636
665
|
var handleSelectFile = function handleSelectFile(path, name, line) {
|
|
@@ -1045,12 +1074,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1045
1074
|
|
|
1046
1075
|
var _debouncedSearch = useRef(window._.debounce(function (q) {
|
|
1047
1076
|
if (!q.trim()) {
|
|
1077
|
+
searchRequestIdRef.current += 1;
|
|
1078
|
+
setSearchLoading(false);
|
|
1048
1079
|
EditorStore.setState({ searchResults: [] });
|
|
1049
1080
|
return;
|
|
1050
1081
|
}
|
|
1082
|
+
var requestId = ++searchRequestIdRef.current;
|
|
1083
|
+
setSearchLoading(true);
|
|
1051
1084
|
EditorStore.setStatus("Searching project...", "info");
|
|
1052
1085
|
SearchService.projectSearch(q).then(function (res) {
|
|
1053
|
-
|
|
1086
|
+
if (searchRequestIdRef.current === requestId) {
|
|
1087
|
+
EditorStore.setStatus("Found " + res.length + " results", "success");
|
|
1088
|
+
}
|
|
1089
|
+
}).finally(function () {
|
|
1090
|
+
if (searchRequestIdRef.current === requestId) {
|
|
1091
|
+
setSearchLoading(false);
|
|
1092
|
+
}
|
|
1054
1093
|
});
|
|
1055
1094
|
}, 400)).current;
|
|
1056
1095
|
|
|
@@ -1060,6 +1099,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1060
1099
|
_debouncedSearch(val);
|
|
1061
1100
|
};
|
|
1062
1101
|
|
|
1102
|
+
var clearSearch = function clearSearch() {
|
|
1103
|
+
searchRequestIdRef.current += 1;
|
|
1104
|
+
if (_debouncedSearch.cancel) _debouncedSearch.cancel();
|
|
1105
|
+
setSearchQuery("");
|
|
1106
|
+
setSearchLoading(false);
|
|
1107
|
+
EditorStore.setState({ searchResults: [] });
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1063
1110
|
var execSearch = function execSearch(e) {
|
|
1064
1111
|
e.preventDefault();
|
|
1065
1112
|
_debouncedSearch(searchQuery);
|
|
@@ -1743,16 +1790,31 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1743
1790
|
React.createElement(
|
|
1744
1791
|
"div",
|
|
1745
1792
|
{ className: "search-input-wrap" },
|
|
1746
|
-
React.createElement(
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1793
|
+
React.createElement(
|
|
1794
|
+
"div",
|
|
1795
|
+
{ className: "search-input-shell" },
|
|
1796
|
+
React.createElement("input", {
|
|
1797
|
+
className: "search-input",
|
|
1798
|
+
placeholder: "Find in files...",
|
|
1799
|
+
value: searchQuery,
|
|
1800
|
+
onChange: handleSearchChange
|
|
1801
|
+
}),
|
|
1802
|
+
searchQuery && React.createElement(
|
|
1803
|
+
"button",
|
|
1804
|
+
{
|
|
1805
|
+
type: "button",
|
|
1806
|
+
className: "search-clear-btn",
|
|
1807
|
+
onClick: clearSearch,
|
|
1808
|
+
title: "Clear search",
|
|
1809
|
+
"aria-label": "Clear search"
|
|
1810
|
+
},
|
|
1811
|
+
React.createElement("i", { className: "fas fa-times" })
|
|
1812
|
+
)
|
|
1813
|
+
),
|
|
1752
1814
|
React.createElement(
|
|
1753
1815
|
"button",
|
|
1754
|
-
{ type: "submit", className: "search-btn" },
|
|
1755
|
-
React.createElement("i", { className: "fas fa-search" })
|
|
1816
|
+
{ type: "submit", className: "search-btn", disabled: searchLoading, title: searchLoading ? "Searching..." : "Search" },
|
|
1817
|
+
React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
|
|
1756
1818
|
)
|
|
1757
1819
|
),
|
|
1758
1820
|
React.createElement(
|
|
@@ -1775,28 +1837,35 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1775
1837
|
"No results"
|
|
1776
1838
|
),
|
|
1777
1839
|
state.searchResults.map(function (res, i) {
|
|
1840
|
+
var fileName = res.file.split('/').pop();
|
|
1778
1841
|
return React.createElement(
|
|
1779
1842
|
"div",
|
|
1780
1843
|
{ key: i, className: "search-result-item", onClick: function () {
|
|
1781
1844
|
return handleSelectFile(res.file, res.file.split('/').pop(), res.line);
|
|
1782
1845
|
} },
|
|
1846
|
+
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
1783
1847
|
React.createElement(
|
|
1784
1848
|
"div",
|
|
1785
|
-
{ className: "search-result-
|
|
1786
|
-
|
|
1787
|
-
|
|
1849
|
+
{ className: "search-result-body" },
|
|
1850
|
+
React.createElement(
|
|
1851
|
+
"div",
|
|
1852
|
+
{ className: "search-result-file" },
|
|
1853
|
+
fileName,
|
|
1854
|
+
React.createElement(
|
|
1855
|
+
"span",
|
|
1856
|
+
{ className: "search-result-line-num" },
|
|
1857
|
+
" ",
|
|
1858
|
+
res.file,
|
|
1859
|
+
":",
|
|
1860
|
+
res.line
|
|
1861
|
+
)
|
|
1862
|
+
),
|
|
1788
1863
|
React.createElement(
|
|
1789
|
-
"
|
|
1790
|
-
{ className: "search-result-
|
|
1791
|
-
|
|
1792
|
-
res.line
|
|
1864
|
+
"div",
|
|
1865
|
+
{ className: "search-result-text" },
|
|
1866
|
+
res.text
|
|
1793
1867
|
)
|
|
1794
1868
|
),
|
|
1795
|
-
React.createElement(
|
|
1796
|
-
"div",
|
|
1797
|
-
{ className: "search-result-text" },
|
|
1798
|
-
res.text
|
|
1799
|
-
)
|
|
1800
1869
|
);
|
|
1801
1870
|
})
|
|
1802
1871
|
)
|
|
@@ -2103,6 +2172,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2103
2172
|
"div",
|
|
2104
2173
|
{ className: "statusbar-msg " + state.statusMessage.kind },
|
|
2105
2174
|
state.statusMessage.text
|
|
2175
|
+
),
|
|
2176
|
+
React.createElement(
|
|
2177
|
+
"div",
|
|
2178
|
+
{ className: "statusbar-version" },
|
|
2179
|
+
"v" + (document.body.dataset.mbeditorVersion || "")
|
|
2106
2180
|
)
|
|
2107
2181
|
),
|
|
2108
2182
|
|