mbeditor 0.3.8 → 0.4.2
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 +35 -0
- data/app/assets/javascripts/mbeditor/application.js +1 -0
- data/app/assets/javascripts/mbeditor/application_iife_head.js +7 -0
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +213 -11
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +14 -4
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +673 -160
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +41 -1
- 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/editor_store.js +10 -2
- data/app/assets/javascripts/mbeditor/file_service.js +29 -23
- data/app/assets/javascripts/mbeditor/git_service.js +7 -11
- data/app/assets/javascripts/mbeditor/search_service.js +51 -14
- data/app/assets/javascripts/mbeditor/tab_manager.js +3 -3
- data/app/assets/javascripts/mbeditor/websocket_service.js +126 -0
- data/app/assets/stylesheets/mbeditor/editor.css +237 -15
- data/app/channels/mbeditor/editor_channel.rb +79 -0
- data/app/controllers/mbeditor/editors_controller.rb +177 -136
- data/app/controllers/mbeditor/git_controller.rb +5 -40
- data/app/services/mbeditor/git_blame_service.rb +6 -0
- data/app/services/mbeditor/git_commit_graph_service.rb +2 -0
- data/app/services/mbeditor/git_service.rb +97 -28
- data/app/services/mbeditor/redmine_service.rb +7 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -2
- data/app/views/layouts/mbeditor/application.html.erb +4 -0
- data/lib/mbeditor/cable_log_filter.rb +28 -0
- data/lib/mbeditor/configuration.rb +7 -1
- data/lib/mbeditor/engine.rb +37 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +4 -1
- data/lib/mbeditor/version.rb +3 -1
- data/lib/mbeditor.rb +2 -0
- metadata +5 -2
|
@@ -24,6 +24,8 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
24
24
|
theme: 'vs-dark',
|
|
25
25
|
fontSize: 13,
|
|
26
26
|
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
27
|
+
lineHeight: 0,
|
|
28
|
+
letterSpacing: 0,
|
|
27
29
|
tabSize: 4,
|
|
28
30
|
insertSpaces: false,
|
|
29
31
|
wordWrap: 'off',
|
|
@@ -32,6 +34,20 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
32
34
|
scrollBeyondLastLine: false,
|
|
33
35
|
minimap: false,
|
|
34
36
|
bracketPairColorization: true,
|
|
37
|
+
renderLineHighlight: 'none',
|
|
38
|
+
cursorStyle: 'line',
|
|
39
|
+
cursorBlinking: 'blink',
|
|
40
|
+
folding: true,
|
|
41
|
+
smoothScrolling: false,
|
|
42
|
+
mouseWheelZoom: false,
|
|
43
|
+
autoClosingBrackets: 'always',
|
|
44
|
+
autoClosingQuotes: 'always',
|
|
45
|
+
autoIndent: 'full',
|
|
46
|
+
formatOnPaste: true,
|
|
47
|
+
formatOnType: true,
|
|
48
|
+
quickSuggestions: true,
|
|
49
|
+
wordBasedSuggestions: 'matchingDocuments',
|
|
50
|
+
acceptSuggestionOnEnter: 'on',
|
|
35
51
|
autoRevealInExplorer: true,
|
|
36
52
|
toolbarIconOnly: false,
|
|
37
53
|
rubocopLintEnabled: true,
|
|
@@ -44,7 +60,9 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
44
60
|
prettierBracketSpacing: true,
|
|
45
61
|
vimMode: false,
|
|
46
62
|
fileTreeTypeahead: true,
|
|
47
|
-
quickOpenShowFolders: false
|
|
63
|
+
quickOpenShowFolders: false,
|
|
64
|
+
tabDisplayMode: 'scroll',
|
|
65
|
+
persistFindState: true
|
|
48
66
|
};
|
|
49
67
|
|
|
50
68
|
var SidebarActionButton = function SidebarActionButton(_ref) {
|
|
@@ -170,20 +188,35 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
170
188
|
var searchHasMore = _useState33h2[0];
|
|
171
189
|
var setSearchHasMore = _useState33h2[1];
|
|
172
190
|
|
|
173
|
-
var
|
|
174
|
-
var
|
|
175
|
-
var
|
|
176
|
-
var
|
|
177
|
-
|
|
178
|
-
var
|
|
179
|
-
var
|
|
191
|
+
var _useState33tc = useState(0);
|
|
192
|
+
var _useState33tc2 = _slicedToArray(_useState33tc, 2);
|
|
193
|
+
var searchTotalCount = _useState33tc2[0];
|
|
194
|
+
var setSearchTotalCount = _useState33tc2[1];
|
|
195
|
+
|
|
196
|
+
var searchHasMoreRef = useRef(false);
|
|
197
|
+
var searchOffsetRef = useRef(0);
|
|
198
|
+
var searchLoadingMoreRef = useRef(false);
|
|
199
|
+
|
|
200
|
+
var _useStateRx = useState(false);
|
|
201
|
+
var _useStateRx2 = _slicedToArray(_useStateRx, 2);
|
|
202
|
+
var searchUseRegex = _useStateRx2[0];
|
|
203
|
+
var setSearchUseRegex = _useStateRx2[1];
|
|
204
|
+
|
|
205
|
+
var _useStateMC = useState(false);
|
|
206
|
+
var _useStateMC2 = _slicedToArray(_useStateMC, 2);
|
|
207
|
+
var searchMatchCase = _useStateMC2[0];
|
|
208
|
+
var setSearchMatchCase = _useStateMC2[1];
|
|
209
|
+
|
|
210
|
+
var _useStateWW = useState(false);
|
|
211
|
+
var _useStateWW2 = _slicedToArray(_useStateWW, 2);
|
|
212
|
+
var searchWholeWord = _useStateWW2[0];
|
|
213
|
+
var setSearchWholeWord = _useStateWW2[1];
|
|
214
|
+
|
|
215
|
+
var searchQueryRef = useRef('');
|
|
216
|
+
var searchUseRegexRef = useRef(false);
|
|
217
|
+
var searchMatchCaseRef = useRef(false);
|
|
218
|
+
var searchWholeWordRef = useRef(false);
|
|
180
219
|
var searchResultsContainerRef = useRef(null);
|
|
181
|
-
var searchVirtStartRef = useRef(0); // first visible item index (for virtual list)
|
|
182
|
-
|
|
183
|
-
var _useState33j = useState(0);
|
|
184
|
-
var _useState33j2 = _slicedToArray(_useState33j, 2);
|
|
185
|
-
var searchVirtStart = _useState33j2[0];
|
|
186
|
-
var setSearchVirtStart = _useState33j2[1];
|
|
187
220
|
|
|
188
221
|
var _useState8 = useState("explorer");
|
|
189
222
|
|
|
@@ -404,6 +437,36 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
404
437
|
var prevGitBranchRef = useRef(null);
|
|
405
438
|
var isSwitchingBranchRef = useRef(false);
|
|
406
439
|
|
|
440
|
+
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
441
|
+
var draftWriteTimerRef = useRef({});
|
|
442
|
+
var serverOnlineRef = useRef(true);
|
|
443
|
+
|
|
444
|
+
var _draftKey = function _draftKey(path) {
|
|
445
|
+
var base = typeof window.mbeditorBasePath === 'function' ? window.mbeditorBasePath() : '';
|
|
446
|
+
return 'mbeditor_draft\x00' + base + '\x00' + path;
|
|
447
|
+
};
|
|
448
|
+
var _saveDraftNow = function _saveDraftNow(path, content) {
|
|
449
|
+
try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
|
|
450
|
+
};
|
|
451
|
+
var _clearDraft = function _clearDraft(path) {
|
|
452
|
+
try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
|
|
453
|
+
};
|
|
454
|
+
var _loadDraft = function _loadDraft(path) {
|
|
455
|
+
try { return JSON.parse(localStorage.getItem(_draftKey(path))); } catch (e) { return null; }
|
|
456
|
+
};
|
|
457
|
+
var _scheduleDraftWrite = function _scheduleDraftWrite(path, content) {
|
|
458
|
+
if (draftWriteTimerRef.current[path]) clearTimeout(draftWriteTimerRef.current[path]);
|
|
459
|
+
draftWriteTimerRef.current[path] = setTimeout(function () {
|
|
460
|
+
delete draftWriteTimerRef.current[path];
|
|
461
|
+
_saveDraftNow(path, content);
|
|
462
|
+
}, 500);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
var _useState_dro = useState(null);
|
|
466
|
+
var _useState_dro2 = _slicedToArray(_useState_dro, 2);
|
|
467
|
+
var draftRestoreOffer = _useState_dro2[0];
|
|
468
|
+
var setDraftRestoreOffer = _useState_dro2[1];
|
|
469
|
+
|
|
407
470
|
var clamp = function clamp(value, min, max) {
|
|
408
471
|
return Math.min(max, Math.max(min, value));
|
|
409
472
|
};
|
|
@@ -621,6 +684,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
621
684
|
if (workspace && typeof workspace.testAvailable === 'boolean') {
|
|
622
685
|
setTestAvailable(workspace.testAvailable);
|
|
623
686
|
}
|
|
687
|
+
if (workspace && typeof workspace.actionCableEnabled === 'boolean') {
|
|
688
|
+
WebSocketService.connect(workspace.actionCableEnabled);
|
|
689
|
+
}
|
|
624
690
|
});
|
|
625
691
|
|
|
626
692
|
// Helper: load tab content for a set of panes and restore them into EditorStore
|
|
@@ -924,11 +990,55 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
924
990
|
return function () { clearTimeout(timeoutId); };
|
|
925
991
|
}, []);
|
|
926
992
|
|
|
993
|
+
// On reconnect: scan open dirty tabs for newer localStorage drafts and offer restore.
|
|
994
|
+
useEffect(function () {
|
|
995
|
+
if (!serverOnline) {
|
|
996
|
+
serverOnlineRef.current = false;
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (serverOnlineRef.current) return; // was already online — no transition
|
|
1000
|
+
serverOnlineRef.current = true;
|
|
1001
|
+
var st = EditorStore.getState();
|
|
1002
|
+
var offers = [];
|
|
1003
|
+
st.panes.forEach(function (pane) {
|
|
1004
|
+
pane.tabs.forEach(function (tab) {
|
|
1005
|
+
if (!tab.dirty || !tab.path || tab.path.startsWith('mbeditor://')) return;
|
|
1006
|
+
var draft = _loadDraft(tab.path);
|
|
1007
|
+
if (draft && draft.content !== tab.content) {
|
|
1008
|
+
offers.push({ paneId: pane.id, tabId: tab.id, path: tab.path, name: tab.name, draftContent: draft.content });
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
if (offers.length > 0) setDraftRestoreOffer(offers);
|
|
1013
|
+
}, [serverOnline]);
|
|
1014
|
+
|
|
1015
|
+
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1016
|
+
// and git status immediately (same work as the 10s poll below does).
|
|
1017
|
+
useEffect(function () {
|
|
1018
|
+
function handleFilesChanged() {
|
|
1019
|
+
if (document.hidden) return;
|
|
1020
|
+
GitService.fetchStatus()["catch"](function () {});
|
|
1021
|
+
FileService.getTree().then(function (data) {
|
|
1022
|
+
var newData = data || [];
|
|
1023
|
+
setTreeData(function (prevData) {
|
|
1024
|
+
if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
|
|
1025
|
+
SearchService.buildIndex(newData);
|
|
1026
|
+
return newData;
|
|
1027
|
+
});
|
|
1028
|
+
})["catch"](function () {});
|
|
1029
|
+
}
|
|
1030
|
+
WebSocketService.onFilesChanged(handleFilesChanged);
|
|
1031
|
+
return function () { WebSocketService.offFilesChanged(handleFilesChanged); };
|
|
1032
|
+
}, []);
|
|
1033
|
+
|
|
927
1034
|
// Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
|
|
1035
|
+
// When an ActionCable WebSocket is connected this acts only as a safety-net fallback —
|
|
1036
|
+
// the WebSocket push above handles immediate invalidation after mbeditor mutations.
|
|
928
1037
|
// Uses functional setTreeData to skip the re-render when nothing has changed.
|
|
929
1038
|
useEffect(function () {
|
|
930
1039
|
var intervalId = setInterval(function () {
|
|
931
1040
|
if (document.hidden) return;
|
|
1041
|
+
if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
|
|
932
1042
|
// Refresh tree and check for git branch changes (to trigger per-branch tab state swap)
|
|
933
1043
|
GitService.fetchStatus()["catch"](function () {});
|
|
934
1044
|
FileService.getTree().then(function (data) {
|
|
@@ -1132,6 +1242,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1132
1242
|
var activeFileCommit = _useState32[0];
|
|
1133
1243
|
var setActiveFileCommit = _useState32[1];
|
|
1134
1244
|
|
|
1245
|
+
// EOL indicator — tracks current line-ending style of the active file
|
|
1246
|
+
var _useState31e = useState(null);
|
|
1247
|
+
var _useState31e2 = _slicedToArray(_useState31e, 2);
|
|
1248
|
+
var activeEOL = _useState31e2[0];
|
|
1249
|
+
var setActiveEOL = _useState31e2[1];
|
|
1250
|
+
|
|
1135
1251
|
useEffect(function () {
|
|
1136
1252
|
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('diff://') === 0 || activeTab.path.indexOf('combined-diff://') === 0) {
|
|
1137
1253
|
setActiveFileCommit(null);
|
|
@@ -1150,6 +1266,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1150
1266
|
});
|
|
1151
1267
|
}, [activeTab ? activeTab.id : null, gitAvailable]);
|
|
1152
1268
|
|
|
1269
|
+
// Update EOL indicator whenever active tab or its content changes
|
|
1270
|
+
useEffect(function () {
|
|
1271
|
+
if (!activeTab || typeof activeTab.content !== 'string' ||
|
|
1272
|
+
activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || activeTab.isPreview) {
|
|
1273
|
+
setActiveEOL(null);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (activeTab.content.indexOf('\r\n') !== -1) {
|
|
1277
|
+
setActiveEOL('CRLF');
|
|
1278
|
+
} else if (activeTab.content.indexOf('\r') !== -1) {
|
|
1279
|
+
setActiveEOL('CR');
|
|
1280
|
+
} else {
|
|
1281
|
+
setActiveEOL('LF');
|
|
1282
|
+
}
|
|
1283
|
+
}, [activeTab ? activeTab.id : null, activeTab ? activeTab.content : null]);
|
|
1284
|
+
|
|
1153
1285
|
useEffect(function () {
|
|
1154
1286
|
if (!activeTab || typeof activeTab.content !== 'string') return;
|
|
1155
1287
|
if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
|
|
@@ -1235,6 +1367,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1235
1367
|
});
|
|
1236
1368
|
EditorStore.setState({ panes: newPanes });
|
|
1237
1369
|
EditorStore.setStatus("Saved", "success");
|
|
1370
|
+
_clearDraft(tab.path);
|
|
1238
1371
|
|
|
1239
1372
|
// Hot reload for Markdown: sync preview tab after save
|
|
1240
1373
|
if (/\.(md|markdown)$/i.test(tab.path)) {
|
|
@@ -1289,16 +1422,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1289
1422
|
var pane2 = EditorStore.getState().panes.find(function (p) {
|
|
1290
1423
|
return p.id === 2;
|
|
1291
1424
|
});
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1425
|
+
var alreadySplit = pane2 && pane2.tabs.length > 0;
|
|
1426
|
+
// Only set the split ref; do NOT pre-split the view width here.
|
|
1427
|
+
// Pane 2 appears as a drop zone only when the cursor actually hovers
|
|
1428
|
+
// over the right-half editor content, keeping the tab bar intact.
|
|
1429
|
+
dragSplitWidthRef.current = alreadySplit ? pane1Width : 50;
|
|
1298
1430
|
setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
|
|
1299
1431
|
};
|
|
1300
1432
|
|
|
1301
1433
|
var clearDragState = function clearDragState() {
|
|
1434
|
+
// If pane 2 is still empty after the drag, restore pane 1 to full width.
|
|
1435
|
+
var pane2 = EditorStore.getState().panes.find(function (p) { return p.id === 2; });
|
|
1436
|
+
if (!pane2 || pane2.tabs.length === 0) {
|
|
1437
|
+
setPane1Width(100);
|
|
1438
|
+
dragSplitWidthRef.current = 50;
|
|
1439
|
+
}
|
|
1302
1440
|
setDraggedTab(null);
|
|
1303
1441
|
setDragOverPaneId(null);
|
|
1304
1442
|
};
|
|
@@ -1309,6 +1447,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1309
1447
|
clearDragState();
|
|
1310
1448
|
};
|
|
1311
1449
|
|
|
1450
|
+
var handleChangeEOL = function handleChangeEOL(newEOL) {
|
|
1451
|
+
var ed = window.__mbeditorActiveEditor;
|
|
1452
|
+
if (!ed || !window.monaco) return;
|
|
1453
|
+
var model = ed.getModel();
|
|
1454
|
+
if (!model || !activeTab || !focusedPane) return;
|
|
1455
|
+
var seq = newEOL === 'CRLF'
|
|
1456
|
+
? window.monaco.editor.EndOfLineSequence.CRLF
|
|
1457
|
+
: window.monaco.editor.EndOfLineSequence.LF;
|
|
1458
|
+
model.setEOL(seq);
|
|
1459
|
+
var newContent = model.getValue();
|
|
1460
|
+
TabManager.markDirty(focusedPane.id, activeTab.path, newContent);
|
|
1461
|
+
setActiveEOL(newEOL);
|
|
1462
|
+
EditorStore.setStatus('Line endings changed to ' + newEOL, 'info');
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1312
1465
|
var handleFormat = function handleFormat() {
|
|
1313
1466
|
if (!activeTab) return;
|
|
1314
1467
|
|
|
@@ -1481,40 +1634,61 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1481
1634
|
var onFormatRef = useRef(handleFormat);
|
|
1482
1635
|
onFormatRef.current = handleFormat;
|
|
1483
1636
|
|
|
1637
|
+
// Eagerly load all remaining pages sequentially in the background.
|
|
1638
|
+
// Self-chains via .then() so only one request is in-flight at a time.
|
|
1639
|
+
// Uses only refs so it's safe to call from async callbacks without
|
|
1640
|
+
// worrying about stale closure state.
|
|
1484
1641
|
var _debouncedSearch = useRef(window._.debounce(function (q) {
|
|
1485
1642
|
if (!q.trim()) {
|
|
1486
1643
|
searchRequestIdRef.current += 1;
|
|
1487
1644
|
setSearchLoading(false);
|
|
1488
|
-
setSearchHasMore(false);
|
|
1645
|
+
setSearchHasMore(false); searchHasMoreRef.current = false;
|
|
1646
|
+
setSearchTotalCount(0);
|
|
1489
1647
|
searchOffsetRef.current = 0;
|
|
1648
|
+
searchLoadingMoreRef.current = false;
|
|
1490
1649
|
searchQueryRef.current = '';
|
|
1491
1650
|
EditorStore.setState({ searchResults: [], searchHasMore: false });
|
|
1492
1651
|
return;
|
|
1493
1652
|
}
|
|
1494
1653
|
var requestId = ++searchRequestIdRef.current;
|
|
1495
1654
|
setSearchLoading(true);
|
|
1496
|
-
setSearchHasMore(false);
|
|
1655
|
+
setSearchHasMore(false); searchHasMoreRef.current = false;
|
|
1656
|
+
setSearchTotalCount(0);
|
|
1497
1657
|
searchOffsetRef.current = 0;
|
|
1658
|
+
searchLoadingMoreRef.current = false;
|
|
1498
1659
|
searchQueryRef.current = q;
|
|
1499
|
-
setSearchVirtStart(0);
|
|
1500
|
-
searchVirtStartRef.current = 0;
|
|
1501
1660
|
EditorStore.setState({ searchResults: [], searchHasMore: false });
|
|
1502
1661
|
EditorStore.setStatus("Searching project...", "info");
|
|
1503
|
-
SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE).then(function (res) {
|
|
1504
|
-
if (searchRequestIdRef.current
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1662
|
+
SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function (res) {
|
|
1663
|
+
if (searchRequestIdRef.current !== requestId) return;
|
|
1664
|
+
var hasMore = !!(res && res.hasMore);
|
|
1665
|
+
setSearchHasMore(hasMore); searchHasMoreRef.current = hasMore;
|
|
1666
|
+
searchOffsetRef.current = SearchService.PAGE_SIZE;
|
|
1667
|
+
if (res && res.totalCount != null) setSearchTotalCount(res.totalCount);
|
|
1668
|
+
var total = (res && res.totalCount != null) ? res.totalCount : (res && res.results ? res.results.length : 0);
|
|
1669
|
+
EditorStore.setStatus("Found " + total + (hasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""), "success");
|
|
1511
1670
|
}).finally(function () {
|
|
1512
|
-
if (searchRequestIdRef.current === requestId)
|
|
1513
|
-
setSearchLoading(false);
|
|
1514
|
-
}
|
|
1671
|
+
if (searchRequestIdRef.current === requestId) setSearchLoading(false);
|
|
1515
1672
|
});
|
|
1516
1673
|
}, 400)).current;
|
|
1517
1674
|
|
|
1675
|
+
var loadMoreSearchResults = function loadMoreSearchResults() {
|
|
1676
|
+
var q = searchQueryRef.current;
|
|
1677
|
+
if (!q || searchLoadingMoreRef.current || !searchHasMoreRef.current) return;
|
|
1678
|
+
searchLoadingMoreRef.current = true;
|
|
1679
|
+
var offset = searchOffsetRef.current;
|
|
1680
|
+
SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function(res) {
|
|
1681
|
+
if (searchQueryRef.current !== q) { searchLoadingMoreRef.current = false; return; }
|
|
1682
|
+
var hasMore = !!(res && res.hasMore);
|
|
1683
|
+
searchHasMoreRef.current = hasMore;
|
|
1684
|
+
setSearchHasMore(hasMore);
|
|
1685
|
+
searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
|
|
1686
|
+
searchLoadingMoreRef.current = false;
|
|
1687
|
+
}).catch(function() {
|
|
1688
|
+
searchLoadingMoreRef.current = false;
|
|
1689
|
+
});
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1518
1692
|
var handleSearchChange = function handleSearchChange(e) {
|
|
1519
1693
|
var val = e.target.value;
|
|
1520
1694
|
if (!val) { clearSearch(); return; }
|
|
@@ -1522,17 +1696,43 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1522
1696
|
_debouncedSearch(val);
|
|
1523
1697
|
};
|
|
1524
1698
|
|
|
1699
|
+
var handleSearchRegexToggle = function handleSearchRegexToggle() {
|
|
1700
|
+
var next = !searchUseRegexRef.current;
|
|
1701
|
+
searchUseRegexRef.current = next;
|
|
1702
|
+
setSearchUseRegex(next);
|
|
1703
|
+
if (searchQueryRef.current) {
|
|
1704
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
var handleSearchMatchCaseToggle = function handleSearchMatchCaseToggle() {
|
|
1709
|
+
var next = !searchMatchCaseRef.current;
|
|
1710
|
+
searchMatchCaseRef.current = next;
|
|
1711
|
+
setSearchMatchCase(next);
|
|
1712
|
+
if (searchQueryRef.current) {
|
|
1713
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
var handleSearchWholeWordToggle = function handleSearchWholeWordToggle() {
|
|
1718
|
+
var next = !searchWholeWordRef.current;
|
|
1719
|
+
searchWholeWordRef.current = next;
|
|
1720
|
+
setSearchWholeWord(next);
|
|
1721
|
+
if (searchQueryRef.current) {
|
|
1722
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1525
1726
|
var clearSearch = function clearSearch() {
|
|
1526
1727
|
searchRequestIdRef.current += 1;
|
|
1527
1728
|
if (_debouncedSearch.cancel) _debouncedSearch.cancel();
|
|
1528
1729
|
setSearchQuery("");
|
|
1529
1730
|
setSearchLoading(false);
|
|
1530
|
-
setSearchHasMore(false);
|
|
1531
|
-
|
|
1731
|
+
setSearchHasMore(false); searchHasMoreRef.current = false;
|
|
1732
|
+
setSearchTotalCount(0);
|
|
1532
1733
|
searchOffsetRef.current = 0;
|
|
1734
|
+
searchLoadingMoreRef.current = false;
|
|
1533
1735
|
searchQueryRef.current = '';
|
|
1534
|
-
setSearchVirtStart(0);
|
|
1535
|
-
searchVirtStartRef.current = 0;
|
|
1536
1736
|
EditorStore.setState({ searchResults: [], searchHasMore: false });
|
|
1537
1737
|
};
|
|
1538
1738
|
|
|
@@ -1541,35 +1741,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1541
1741
|
_debouncedSearch(searchQuery);
|
|
1542
1742
|
};
|
|
1543
1743
|
|
|
1544
|
-
|
|
1545
|
-
var q = searchQueryRef.current;
|
|
1546
|
-
if (!q || searchLoadingMore || !searchHasMore) return;
|
|
1547
|
-
var offset = searchOffsetRef.current;
|
|
1548
|
-
setSearchLoadingMore(true);
|
|
1549
|
-
SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function (res) {
|
|
1550
|
-
var hasMore = !!(res && res.hasMore);
|
|
1551
|
-
setSearchHasMore(hasMore);
|
|
1552
|
-
searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
|
|
1553
|
-
}).finally(function () {
|
|
1554
|
-
setSearchLoadingMore(false);
|
|
1555
|
-
});
|
|
1556
|
-
};
|
|
1557
|
-
|
|
1558
|
-
// Scroll handler for the virtualized search results list.
|
|
1559
|
-
var SEARCH_ITEM_H = 40;
|
|
1560
|
-
var SEARCH_OVERSCAN = 8;
|
|
1744
|
+
// Load more results when the user scrolls near the bottom of the list.
|
|
1561
1745
|
var handleSearchResultsScroll = function handleSearchResultsScroll(e) {
|
|
1562
1746
|
var el = e.currentTarget;
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
searchVirtStartRef.current = newStart;
|
|
1566
|
-
setSearchVirtStart(newStart);
|
|
1567
|
-
}
|
|
1568
|
-
// Trigger load-more when within 200px of the bottom
|
|
1569
|
-
if (!searchLoadingMore && searchHasMore) {
|
|
1570
|
-
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
1571
|
-
loadMoreSearchResults();
|
|
1572
|
-
}
|
|
1747
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
1748
|
+
loadMoreSearchResults();
|
|
1573
1749
|
}
|
|
1574
1750
|
};
|
|
1575
1751
|
|
|
@@ -2053,6 +2229,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2053
2229
|
tabs: tabs,
|
|
2054
2230
|
activeId: activeId,
|
|
2055
2231
|
paneId: paneId,
|
|
2232
|
+
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
2056
2233
|
onSelect: function (id) {
|
|
2057
2234
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
2058
2235
|
var tab = tabs.find(function(t) { return t.id === id; });
|
|
@@ -2495,116 +2672,120 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2495
2672
|
{ className: "search-panel" },
|
|
2496
2673
|
React.createElement(
|
|
2497
2674
|
"div",
|
|
2498
|
-
{ className: "search-input-
|
|
2675
|
+
{ className: "search-input-shell" },
|
|
2676
|
+
React.createElement("input", {
|
|
2677
|
+
className: "search-input",
|
|
2678
|
+
placeholder: "Find in files…",
|
|
2679
|
+
value: searchQuery,
|
|
2680
|
+
onChange: handleSearchChange
|
|
2681
|
+
}),
|
|
2499
2682
|
React.createElement(
|
|
2500
2683
|
"div",
|
|
2501
|
-
{ className: "search-input-
|
|
2502
|
-
React.createElement(
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2684
|
+
{ className: "search-input-adornments" },
|
|
2685
|
+
React.createElement(
|
|
2686
|
+
"button",
|
|
2687
|
+
{
|
|
2688
|
+
type: "button",
|
|
2689
|
+
className: "search-adornment-btn" + (searchMatchCase ? " active" : ""),
|
|
2690
|
+
onClick: handleSearchMatchCaseToggle,
|
|
2691
|
+
title: "Match Case"
|
|
2692
|
+
},
|
|
2693
|
+
React.createElement("i", { className: "codicon codicon-case-sensitive" })
|
|
2694
|
+
),
|
|
2695
|
+
React.createElement(
|
|
2696
|
+
"button",
|
|
2697
|
+
{
|
|
2698
|
+
type: "button",
|
|
2699
|
+
className: "search-adornment-btn" + (searchWholeWord ? " active" : ""),
|
|
2700
|
+
onClick: handleSearchWholeWordToggle,
|
|
2701
|
+
title: "Match Whole Word"
|
|
2702
|
+
},
|
|
2703
|
+
React.createElement("i", { className: "codicon codicon-whole-word" })
|
|
2704
|
+
),
|
|
2705
|
+
React.createElement(
|
|
2706
|
+
"button",
|
|
2707
|
+
{
|
|
2708
|
+
type: "button",
|
|
2709
|
+
className: "search-adornment-btn" + (searchUseRegex ? " active" : ""),
|
|
2710
|
+
onClick: handleSearchRegexToggle,
|
|
2711
|
+
title: "Use Regular Expression"
|
|
2712
|
+
},
|
|
2713
|
+
React.createElement("i", { className: "codicon codicon-regex" })
|
|
2714
|
+
),
|
|
2508
2715
|
searchQuery && React.createElement(
|
|
2509
2716
|
"button",
|
|
2510
2717
|
{
|
|
2511
2718
|
type: "button",
|
|
2512
|
-
className: "search-
|
|
2719
|
+
className: "search-adornment-btn search-adornment-clear",
|
|
2513
2720
|
onClick: clearSearch,
|
|
2514
2721
|
title: "Clear search",
|
|
2515
2722
|
"aria-label": "Clear search"
|
|
2516
2723
|
},
|
|
2517
2724
|
React.createElement("i", { className: "fas fa-times" })
|
|
2518
2725
|
)
|
|
2519
|
-
),
|
|
2520
|
-
React.createElement(
|
|
2521
|
-
"button",
|
|
2522
|
-
{ type: "submit", className: "search-btn", disabled: searchLoading, title: searchLoading ? "Searching..." : "Search" },
|
|
2523
|
-
React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
|
|
2524
2726
|
)
|
|
2525
2727
|
),
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
onScroll: handleSearchResultsScroll
|
|
2532
|
-
},
|
|
2533
|
-
(function() {
|
|
2534
|
-
var allResults = state.searchResults || [];
|
|
2535
|
-
var totalCount = allResults.length;
|
|
2536
|
-
|
|
2537
|
-
if (searchQuery && totalCount === 0 && !searchLoading) {
|
|
2538
|
-
return React.createElement(
|
|
2539
|
-
"div", { className: "search-results-empty" }, "No results"
|
|
2540
|
-
);
|
|
2541
|
-
}
|
|
2728
|
+
(function() {
|
|
2729
|
+
var allResults = state.searchResults || [];
|
|
2730
|
+
var loadedCount = allResults.length;
|
|
2731
|
+
var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
|
|
2732
|
+
var hasAny = loadedCount > 0;
|
|
2542
2733
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
return React.createElement(
|
|
2734
|
+
return React.createElement(
|
|
2735
|
+
React.Fragment,
|
|
2736
|
+
null,
|
|
2737
|
+
searchQuery && !searchLoading && React.createElement(
|
|
2738
|
+
"div",
|
|
2739
|
+
{ className: "search-results-header" },
|
|
2740
|
+
hasAny
|
|
2741
|
+
? (total + (searchHasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""))
|
|
2742
|
+
: "No results"
|
|
2743
|
+
),
|
|
2744
|
+
React.createElement(
|
|
2745
|
+
"div",
|
|
2746
|
+
{ className: "search-results-area" },
|
|
2747
|
+
hasAny && React.createElement(
|
|
2558
2748
|
"div",
|
|
2559
2749
|
{
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2750
|
+
className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
|
|
2751
|
+
ref: searchResultsContainerRef,
|
|
2752
|
+
onScroll: handleSearchResultsScroll
|
|
2563
2753
|
},
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
{ className: "search-result-body" },
|
|
2568
|
-
React.createElement(
|
|
2754
|
+
allResults.map(function(res, i) {
|
|
2755
|
+
var fileName = res.file.split('/').pop();
|
|
2756
|
+
return React.createElement(
|
|
2569
2757
|
"div",
|
|
2570
|
-
{
|
|
2571
|
-
|
|
2758
|
+
{
|
|
2759
|
+
key: i,
|
|
2760
|
+
className: "search-result-item",
|
|
2761
|
+
onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
|
|
2762
|
+
},
|
|
2763
|
+
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
2572
2764
|
React.createElement(
|
|
2573
|
-
"
|
|
2574
|
-
|
|
2575
|
-
|
|
2765
|
+
"div", { className: "search-result-body" },
|
|
2766
|
+
React.createElement(
|
|
2767
|
+
"div", { className: "search-result-file" },
|
|
2768
|
+
fileName,
|
|
2769
|
+
React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
|
|
2770
|
+
),
|
|
2771
|
+
React.createElement("div", { className: "search-result-text" }, res.text)
|
|
2576
2772
|
)
|
|
2577
|
-
)
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2773
|
+
);
|
|
2774
|
+
}),
|
|
2775
|
+
searchHasMore && React.createElement(
|
|
2776
|
+
"div", { className: "search-loading-more" },
|
|
2777
|
+
React.createElement("i", { className: "fas fa-spinner fa-spin" }),
|
|
2778
|
+
" Loading more\u2026"
|
|
2583
2779
|
)
|
|
2584
|
-
);
|
|
2585
|
-
});
|
|
2586
|
-
|
|
2587
|
-
return React.createElement(
|
|
2588
|
-
React.Fragment,
|
|
2589
|
-
null,
|
|
2590
|
-
React.createElement(
|
|
2591
|
-
"div",
|
|
2592
|
-
{ className: "search-results-meta" },
|
|
2593
|
-
totalCount,
|
|
2594
|
-
" result" + (totalCount !== 1 ? "s" : ""),
|
|
2595
|
-
searchHasMore && React.createElement("span", { className: "search-results-capped" }, " — more available, scroll for next page")
|
|
2596
2780
|
),
|
|
2597
|
-
React.createElement(
|
|
2598
|
-
visibleItems,
|
|
2599
|
-
React.createElement("div", { style: { height: paddingBottom + 'px', flexShrink: 0 } }),
|
|
2600
|
-
searchLoadingMore && React.createElement(
|
|
2781
|
+
searchLoading && React.createElement(
|
|
2601
2782
|
"div",
|
|
2602
|
-
{ className: "search-
|
|
2603
|
-
|
|
2783
|
+
{ className: "search-loading-overlay" },
|
|
2784
|
+
React.createElement("div", { className: "search-loading-spinner" })
|
|
2604
2785
|
)
|
|
2605
|
-
)
|
|
2606
|
-
|
|
2607
|
-
)
|
|
2786
|
+
)
|
|
2787
|
+
);
|
|
2788
|
+
})()
|
|
2608
2789
|
)
|
|
2609
2790
|
)
|
|
2610
2791
|
),
|
|
@@ -2625,16 +2806,40 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2625
2806
|
if (!draggedTab) return;
|
|
2626
2807
|
e.preventDefault();
|
|
2627
2808
|
|
|
2809
|
+
// If the cursor is over the tab bar, suppress the cross-pane split overlay
|
|
2810
|
+
// so same-pane tab reordering within any tab bar is unaffected.
|
|
2811
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2812
|
+
if (dragOverPaneId !== null) setDragOverPaneId(null);
|
|
2813
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2628
2817
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
2629
2818
|
var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
|
|
2630
2819
|
var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
|
|
2631
2820
|
var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
|
|
2632
2821
|
|
|
2633
|
-
|
|
2822
|
+
// When cursor first enters the right-half content area and pane 2 is empty,
|
|
2823
|
+
// apply the 50% split width so the drop zone becomes visible.
|
|
2824
|
+
if (nextDropPane === 2) {
|
|
2825
|
+
var pane2Empty = EditorStore.getState().panes.find(function(p) { return p.id === 2; });
|
|
2826
|
+
if (!pane2Empty || pane2Empty.tabs.length === 0) {
|
|
2827
|
+
setPane1Width(50);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2634
2832
|
if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
|
|
2635
2833
|
},
|
|
2636
2834
|
onDropCapture: function (e) {
|
|
2637
2835
|
if (!draggedTab) return;
|
|
2836
|
+
|
|
2837
|
+
// If dropping onto a tab bar element, let the tab item's own onDrop
|
|
2838
|
+
// bubble-phase handler manage the reorder — don't intercept here.
|
|
2839
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2638
2843
|
e.preventDefault();
|
|
2639
2844
|
|
|
2640
2845
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
@@ -2649,10 +2854,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2649
2854
|
}
|
|
2650
2855
|
},
|
|
2651
2856
|
state.panes.map(function (pane, idx) {
|
|
2652
|
-
|
|
2857
|
+
// Show empty pane 2 as a drop zone only when the cursor is actively hovering
|
|
2858
|
+
// over its half of the editor content (dragOverPaneId === 2).
|
|
2859
|
+
if (pane.id === 2 && pane.tabs.length === 0 && dragOverPaneId !== 2) return null;
|
|
2653
2860
|
|
|
2654
2861
|
// Dynamic width distribution
|
|
2655
|
-
var isSplit = state.panes[1].tabs.length > 0 ||
|
|
2862
|
+
var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
|
|
2656
2863
|
var flexBasis = '100%';
|
|
2657
2864
|
if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
|
|
2658
2865
|
|
|
@@ -2735,6 +2942,32 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2735
2942
|
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
|
|
2736
2943
|
})
|
|
2737
2944
|
),
|
|
2945
|
+
React.createElement(
|
|
2946
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)' },
|
|
2947
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Line height (0=auto)'),
|
|
2948
|
+
React.createElement('input', {
|
|
2949
|
+
type: 'number', min: '0', max: '100', step: '1',
|
|
2950
|
+
className: 'ide-settings-input',
|
|
2951
|
+
value: editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0,
|
|
2952
|
+
onChange: function(e) {
|
|
2953
|
+
var v = parseInt(e.target.value, 10);
|
|
2954
|
+
if (!isNaN(v) && v >= 0 && v <= 100) setEditorPrefs(function(p) { return Object.assign({}, p, { lineHeight: v }); });
|
|
2955
|
+
}
|
|
2956
|
+
})
|
|
2957
|
+
),
|
|
2958
|
+
React.createElement(
|
|
2959
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Extra space between characters in pixels. 0 = default' },
|
|
2960
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Letter spacing (px)'),
|
|
2961
|
+
React.createElement('input', {
|
|
2962
|
+
type: 'number', min: '-5', max: '20', step: '0.5',
|
|
2963
|
+
className: 'ide-settings-input',
|
|
2964
|
+
value: editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0,
|
|
2965
|
+
onChange: function(e) {
|
|
2966
|
+
var v = parseFloat(e.target.value);
|
|
2967
|
+
if (!isNaN(v) && v >= -5 && v <= 20) setEditorPrefs(function(p) { return Object.assign({}, p, { letterSpacing: v }); });
|
|
2968
|
+
}
|
|
2969
|
+
})
|
|
2970
|
+
),
|
|
2738
2971
|
|
|
2739
2972
|
/* ── Indentation (unified editor + Prettier) ── */
|
|
2740
2973
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
|
|
@@ -2844,6 +3077,184 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2844
3077
|
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { vimMode: v }); }); }
|
|
2845
3078
|
})
|
|
2846
3079
|
),
|
|
3080
|
+
React.createElement(
|
|
3081
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing bracket automatically' },
|
|
3082
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close brackets'),
|
|
3083
|
+
React.createElement(
|
|
3084
|
+
'select', {
|
|
3085
|
+
value: editorPrefs.autoClosingBrackets || 'always',
|
|
3086
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingBrackets: e.target.value }); }); }
|
|
3087
|
+
},
|
|
3088
|
+
React.createElement('option', { value: 'always' }, 'Always'),
|
|
3089
|
+
React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
|
|
3090
|
+
React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
|
|
3091
|
+
React.createElement('option', { value: 'never' }, 'Never')
|
|
3092
|
+
)
|
|
3093
|
+
),
|
|
3094
|
+
React.createElement(
|
|
3095
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing quote automatically' },
|
|
3096
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close quotes'),
|
|
3097
|
+
React.createElement(
|
|
3098
|
+
'select', {
|
|
3099
|
+
value: editorPrefs.autoClosingQuotes || 'always',
|
|
3100
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingQuotes: e.target.value }); }); }
|
|
3101
|
+
},
|
|
3102
|
+
React.createElement('option', { value: 'always' }, 'Always'),
|
|
3103
|
+
React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
|
|
3104
|
+
React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
|
|
3105
|
+
React.createElement('option', { value: 'never' }, 'Never')
|
|
3106
|
+
)
|
|
3107
|
+
),
|
|
3108
|
+
React.createElement(
|
|
3109
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'What to highlight on the current editor line' },
|
|
3110
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Line highlight'),
|
|
3111
|
+
React.createElement(
|
|
3112
|
+
'select', {
|
|
3113
|
+
value: editorPrefs.renderLineHighlight || 'none',
|
|
3114
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderLineHighlight: e.target.value }); }); }
|
|
3115
|
+
},
|
|
3116
|
+
React.createElement('option', { value: 'none' }, 'None'),
|
|
3117
|
+
React.createElement('option', { value: 'gutter' }, 'Line number only'),
|
|
3118
|
+
React.createElement('option', { value: 'line' }, 'Current line background'),
|
|
3119
|
+
React.createElement('option', { value: 'all' }, 'Line number + background')
|
|
3120
|
+
)
|
|
3121
|
+
),
|
|
3122
|
+
React.createElement(
|
|
3123
|
+
'label', { className: 'ide-settings-row ide-settings-row-half' },
|
|
3124
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
|
|
3125
|
+
React.createElement(
|
|
3126
|
+
'select', {
|
|
3127
|
+
value: editorPrefs.cursorStyle || 'line',
|
|
3128
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorStyle: e.target.value }); }); }
|
|
3129
|
+
},
|
|
3130
|
+
React.createElement('option', { value: 'line' }, 'Line (|)'),
|
|
3131
|
+
React.createElement('option', { value: 'block' }, 'Block (filled)'),
|
|
3132
|
+
React.createElement('option', { value: 'underline' }, 'Underline (_)'),
|
|
3133
|
+
React.createElement('option', { value: 'line-thin' }, 'Line thin'),
|
|
3134
|
+
React.createElement('option', { value: 'block-outline' }, 'Block outline'),
|
|
3135
|
+
React.createElement('option', { value: 'underline-thin' }, 'Underline thin')
|
|
3136
|
+
)
|
|
3137
|
+
),
|
|
3138
|
+
React.createElement(
|
|
3139
|
+
'label', { className: 'ide-settings-row ide-settings-row-half' },
|
|
3140
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
|
|
3141
|
+
React.createElement(
|
|
3142
|
+
'select', {
|
|
3143
|
+
value: editorPrefs.cursorBlinking || 'blink',
|
|
3144
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorBlinking: e.target.value }); }); }
|
|
3145
|
+
},
|
|
3146
|
+
React.createElement('option', { value: 'blink' }, 'Blink (on/off)'),
|
|
3147
|
+
React.createElement('option', { value: 'smooth' }, 'Smooth (fade)'),
|
|
3148
|
+
React.createElement('option', { value: 'phase' }, 'Phase (offset fade)'),
|
|
3149
|
+
React.createElement('option', { value: 'expand' }, 'Expand (grow/shrink)'),
|
|
3150
|
+
React.createElement('option', { value: 'solid' }, 'Solid (no blink)')
|
|
3151
|
+
)
|
|
3152
|
+
),
|
|
3153
|
+
React.createElement(
|
|
3154
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)' },
|
|
3155
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Code folding'),
|
|
3156
|
+
React.createElement('input', {
|
|
3157
|
+
type: 'checkbox',
|
|
3158
|
+
className: 'ide-settings-checkbox',
|
|
3159
|
+
checked: editorPrefs.folding !== false,
|
|
3160
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { folding: v }); }); }
|
|
3161
|
+
})
|
|
3162
|
+
),
|
|
3163
|
+
React.createElement(
|
|
3164
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Animate scrolling instead of jumping instantly' },
|
|
3165
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Smooth scrolling'),
|
|
3166
|
+
React.createElement('input', {
|
|
3167
|
+
type: 'checkbox',
|
|
3168
|
+
className: 'ide-settings-checkbox',
|
|
3169
|
+
checked: !!(editorPrefs.smoothScrolling),
|
|
3170
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { smoothScrolling: v }); }); }
|
|
3171
|
+
})
|
|
3172
|
+
),
|
|
3173
|
+
React.createElement(
|
|
3174
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
|
|
3175
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Ctrl+scroll to zoom'),
|
|
3176
|
+
React.createElement('input', {
|
|
3177
|
+
type: 'checkbox',
|
|
3178
|
+
className: 'ide-settings-checkbox',
|
|
3179
|
+
checked: !!(editorPrefs.mouseWheelZoom),
|
|
3180
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { mouseWheelZoom: v }); }); }
|
|
3181
|
+
})
|
|
3182
|
+
),
|
|
3183
|
+
|
|
3184
|
+
/* ── Behaviour ───────────────────────────────── */
|
|
3185
|
+
React.createElement('div', { className: 'ide-settings-section-header' }, 'Behaviour'),
|
|
3186
|
+
React.createElement(
|
|
3187
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How aggressively the editor re-indents lines as you type' },
|
|
3188
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Auto indent'),
|
|
3189
|
+
React.createElement(
|
|
3190
|
+
'select', {
|
|
3191
|
+
value: editorPrefs.autoIndent || 'full',
|
|
3192
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoIndent: e.target.value }); }); }
|
|
3193
|
+
},
|
|
3194
|
+
React.createElement('option', { value: 'none' }, 'None (disabled)'),
|
|
3195
|
+
React.createElement('option', { value: 'keep' }, 'Keep current level'),
|
|
3196
|
+
React.createElement('option', { value: 'brackets' }, 'Indent on { and ['),
|
|
3197
|
+
React.createElement('option', { value: 'advanced' }, 'Language indent rules'),
|
|
3198
|
+
React.createElement('option', { value: 'full' }, 'Full (language grammar)')
|
|
3199
|
+
)
|
|
3200
|
+
),
|
|
3201
|
+
React.createElement(
|
|
3202
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion' },
|
|
3203
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Accept suggestion on Enter'),
|
|
3204
|
+
React.createElement(
|
|
3205
|
+
'select', {
|
|
3206
|
+
value: editorPrefs.acceptSuggestionOnEnter || 'on',
|
|
3207
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { acceptSuggestionOnEnter: e.target.value }); }); }
|
|
3208
|
+
},
|
|
3209
|
+
React.createElement('option', { value: 'on' }, 'Always'),
|
|
3210
|
+
React.createElement('option', { value: 'smart' }, 'Only when navigated (↑↓)'),
|
|
3211
|
+
React.createElement('option', { value: 'off' }, 'Never (Tab only)')
|
|
3212
|
+
)
|
|
3213
|
+
),
|
|
3214
|
+
React.createElement(
|
|
3215
|
+
'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Suggest completions based on words already present in open files' },
|
|
3216
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Word-based suggestions'),
|
|
3217
|
+
React.createElement(
|
|
3218
|
+
'select', {
|
|
3219
|
+
value: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
|
|
3220
|
+
onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordBasedSuggestions: e.target.value }); }); }
|
|
3221
|
+
},
|
|
3222
|
+
React.createElement('option', { value: 'off' }, 'Off'),
|
|
3223
|
+
React.createElement('option', { value: 'currentDocument' }, 'Current file only'),
|
|
3224
|
+
React.createElement('option', { value: 'matchingDocuments' }, 'Same language files'),
|
|
3225
|
+
React.createElement('option', { value: 'allDocuments' }, 'All open files')
|
|
3226
|
+
)
|
|
3227
|
+
),
|
|
3228
|
+
React.createElement(
|
|
3229
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Auto-format pasted code using the language formatter' },
|
|
3230
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
|
|
3231
|
+
React.createElement('input', {
|
|
3232
|
+
type: 'checkbox',
|
|
3233
|
+
className: 'ide-settings-checkbox',
|
|
3234
|
+
checked: editorPrefs.formatOnPaste !== false,
|
|
3235
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
|
|
3236
|
+
})
|
|
3237
|
+
),
|
|
3238
|
+
React.createElement(
|
|
3239
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Re-indent and auto-close blocks as you type (e.g. after pressing Enter inside {})' },
|
|
3240
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
|
|
3241
|
+
React.createElement('input', {
|
|
3242
|
+
type: 'checkbox',
|
|
3243
|
+
className: 'ide-settings-checkbox',
|
|
3244
|
+
checked: editorPrefs.formatOnType !== false,
|
|
3245
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnType: v }); }); }
|
|
3246
|
+
})
|
|
3247
|
+
),
|
|
3248
|
+
React.createElement(
|
|
3249
|
+
'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)' },
|
|
3250
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Quick suggestions'),
|
|
3251
|
+
React.createElement('input', {
|
|
3252
|
+
type: 'checkbox',
|
|
3253
|
+
className: 'ide-settings-checkbox',
|
|
3254
|
+
checked: editorPrefs.quickSuggestions !== false,
|
|
3255
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickSuggestions: v }); }); }
|
|
3256
|
+
})
|
|
3257
|
+
),
|
|
2847
3258
|
|
|
2848
3259
|
/* ── Formatting (Prettier) ───────────────────── */
|
|
2849
3260
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
|
|
@@ -2926,6 +3337,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2926
3337
|
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
|
|
2927
3338
|
})
|
|
2928
3339
|
),
|
|
3340
|
+
React.createElement(
|
|
3341
|
+
'label', { className: 'ide-settings-row ide-settings-row-half' },
|
|
3342
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
|
|
3343
|
+
React.createElement(
|
|
3344
|
+
'select', {
|
|
3345
|
+
value: editorPrefs.tabDisplayMode || 'scroll',
|
|
3346
|
+
onChange: function(e) { var v = e.target.value; setEditorPrefs(function(p) { return Object.assign({}, p, { tabDisplayMode: v }); }); }
|
|
3347
|
+
},
|
|
3348
|
+
React.createElement('option', { value: 'scroll' }, 'Scroll'),
|
|
3349
|
+
React.createElement('option', { value: 'wrap' }, 'Wrap (multi-row)')
|
|
3350
|
+
)
|
|
3351
|
+
),
|
|
2929
3352
|
React.createElement(
|
|
2930
3353
|
'label', { className: 'ide-settings-row ide-settings-row-check' },
|
|
2931
3354
|
React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
|
|
@@ -2947,6 +3370,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2947
3370
|
})
|
|
2948
3371
|
),
|
|
2949
3372
|
|
|
3373
|
+
React.createElement(
|
|
3374
|
+
'label', { className: 'ide-settings-row ide-settings-row-check' },
|
|
3375
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Persist find state across files'),
|
|
3376
|
+
React.createElement('input', {
|
|
3377
|
+
type: 'checkbox',
|
|
3378
|
+
className: 'ide-settings-checkbox',
|
|
3379
|
+
checked: editorPrefs.persistFindState !== false,
|
|
3380
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { persistFindState: v }); }); }
|
|
3381
|
+
})
|
|
3382
|
+
),
|
|
3383
|
+
|
|
2950
3384
|
/* ── RuboCop ─────────────────────────────────── */
|
|
2951
3385
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
|
|
2952
3386
|
React.createElement(
|
|
@@ -3023,8 +3457,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3023
3457
|
var valNorm = val.replace(/\r\n/g, '\n');
|
|
3024
3458
|
if (valNorm === cleanNorm) {
|
|
3025
3459
|
TabManager.markClean(pane.id, pActiveTab.id, val);
|
|
3460
|
+
_clearDraft(pActiveTab.path);
|
|
3026
3461
|
} else {
|
|
3027
3462
|
TabManager.markDirty(pane.id, pActiveTab.id, val);
|
|
3463
|
+
_scheduleDraftWrite(pActiveTab.path, val);
|
|
3028
3464
|
}
|
|
3029
3465
|
}
|
|
3030
3466
|
});
|
|
@@ -3226,12 +3662,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3226
3662
|
state.gitInfo.behind
|
|
3227
3663
|
)
|
|
3228
3664
|
),
|
|
3229
|
-
!serverOnline &&
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3665
|
+
!serverOnline && (function () {
|
|
3666
|
+
var dirtyCount = state.panes.reduce(function (acc, p) {
|
|
3667
|
+
return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
|
|
3668
|
+
}, 0);
|
|
3669
|
+
return React.createElement(
|
|
3670
|
+
"div",
|
|
3671
|
+
{
|
|
3672
|
+
className: "statusbar-offline",
|
|
3673
|
+
title: dirtyCount > 0 ? dirtyCount + " unsaved file" + (dirtyCount !== 1 ? "s" : "") + " — changes are backed up locally" : "Server offline"
|
|
3674
|
+
},
|
|
3675
|
+
React.createElement("i", { className: "fas fa-exclamation-triangle" }),
|
|
3676
|
+
dirtyCount > 0
|
|
3677
|
+
? " Offline \u2014 " + dirtyCount + " unsaved"
|
|
3678
|
+
: " Server offline"
|
|
3679
|
+
);
|
|
3680
|
+
})(),
|
|
3235
3681
|
activeFileCommit && React.createElement(
|
|
3236
3682
|
"div",
|
|
3237
3683
|
{ className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
|
|
@@ -3245,6 +3691,16 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3245
3691
|
{ className: "statusbar-msg " + state.statusMessage.kind },
|
|
3246
3692
|
state.statusMessage.text
|
|
3247
3693
|
),
|
|
3694
|
+
activeEOL && React.createElement(
|
|
3695
|
+
"button",
|
|
3696
|
+
{
|
|
3697
|
+
type: "button",
|
|
3698
|
+
className: "statusbar-btn statusbar-eol-btn",
|
|
3699
|
+
title: "Line endings: " + activeEOL + " — click to change",
|
|
3700
|
+
onClick: function() { handleChangeEOL(activeEOL === 'CRLF' ? 'LF' : 'CRLF'); }
|
|
3701
|
+
},
|
|
3702
|
+
activeEOL
|
|
3703
|
+
),
|
|
3248
3704
|
React.createElement(
|
|
3249
3705
|
"div",
|
|
3250
3706
|
{ className: "statusbar-version" },
|
|
@@ -3366,6 +3822,63 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3366
3822
|
onSelectFolder: handleOpenFolderInExplorer,
|
|
3367
3823
|
onClose: function () { return setQuickOpen(false); }
|
|
3368
3824
|
}),
|
|
3825
|
+
draftRestoreOffer && React.createElement(
|
|
3826
|
+
"div",
|
|
3827
|
+
{
|
|
3828
|
+
className: "ide-draft-restore-overlay",
|
|
3829
|
+
role: "dialog",
|
|
3830
|
+
"aria-modal": "true",
|
|
3831
|
+
"aria-label": "Restore unsaved drafts"
|
|
3832
|
+
},
|
|
3833
|
+
React.createElement(
|
|
3834
|
+
"div",
|
|
3835
|
+
{ className: "ide-draft-restore-dialog" },
|
|
3836
|
+
React.createElement("div", { className: "ide-draft-restore-title" },
|
|
3837
|
+
React.createElement("i", { className: "fas fa-save", style: { marginRight: 8 } }),
|
|
3838
|
+
"Unsaved drafts found"
|
|
3839
|
+
),
|
|
3840
|
+
React.createElement("div", { className: "ide-draft-restore-body" },
|
|
3841
|
+
draftRestoreOffer.length + " file" + (draftRestoreOffer.length !== 1 ? "s have" : " has") + " locally backed-up drafts from when the server was offline:"
|
|
3842
|
+
),
|
|
3843
|
+
React.createElement(
|
|
3844
|
+
"ul",
|
|
3845
|
+
{ className: "ide-draft-restore-list" },
|
|
3846
|
+
draftRestoreOffer.map(function (o) {
|
|
3847
|
+
return React.createElement("li", { key: o.path }, o.name || o.path);
|
|
3848
|
+
})
|
|
3849
|
+
),
|
|
3850
|
+
React.createElement(
|
|
3851
|
+
"div",
|
|
3852
|
+
{ className: "ide-draft-restore-actions" },
|
|
3853
|
+
React.createElement(
|
|
3854
|
+
"button",
|
|
3855
|
+
{
|
|
3856
|
+
type: "button",
|
|
3857
|
+
className: "ide-draft-restore-btn ide-draft-restore-btn-primary",
|
|
3858
|
+
onClick: function () {
|
|
3859
|
+
draftRestoreOffer.forEach(function (offer) {
|
|
3860
|
+
TabManager.markDirty(offer.paneId, offer.tabId, offer.draftContent);
|
|
3861
|
+
});
|
|
3862
|
+
setDraftRestoreOffer(null);
|
|
3863
|
+
}
|
|
3864
|
+
},
|
|
3865
|
+
"Restore all"
|
|
3866
|
+
),
|
|
3867
|
+
React.createElement(
|
|
3868
|
+
"button",
|
|
3869
|
+
{
|
|
3870
|
+
type: "button",
|
|
3871
|
+
className: "ide-draft-restore-btn",
|
|
3872
|
+
onClick: function () {
|
|
3873
|
+
draftRestoreOffer.forEach(function (offer) { _clearDraft(offer.path); });
|
|
3874
|
+
setDraftRestoreOffer(null);
|
|
3875
|
+
}
|
|
3876
|
+
},
|
|
3877
|
+
"Discard drafts"
|
|
3878
|
+
)
|
|
3879
|
+
)
|
|
3880
|
+
)
|
|
3881
|
+
),
|
|
3369
3882
|
contextMenu && React.createElement(
|
|
3370
3883
|
React.Fragment,
|
|
3371
3884
|
null,
|