mbeditor 0.12.9 → 0.13.1
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 +216 -1
- data/app/assets/javascripts/mbeditor/application.js +6 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +52 -1
- data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +104 -103
- data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +224 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +704 -270
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +193 -9
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +105 -77
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +434 -112
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +112 -18
- data/app/assets/javascripts/mbeditor/git_service.js +130 -8
- data/app/assets/javascripts/mbeditor/history_service.js +33 -38
- data/app/assets/javascripts/mbeditor/log_service.js +4 -0
- data/app/assets/javascripts/mbeditor/search_service.js +50 -4
- data/app/assets/javascripts/mbeditor/tab_manager.js +66 -10
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +389 -103
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/collaboration_channel.rb +8 -2
- data/app/channels/mbeditor/editor_channel.rb +6 -3
- data/app/controllers/mbeditor/application_controller.rb +5 -0
- data/app/controllers/mbeditor/editors_controller.rb +155 -29
- data/app/controllers/mbeditor/git_controller.rb +3 -3
- data/app/controllers/mbeditor/logs_controller.rb +3 -1
- data/app/services/mbeditor/collaboration_doc_store.rb +49 -3
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- data/app/services/mbeditor/editor_state_service.rb +36 -26
- data/app/services/mbeditor/exclusion_matcher.rb +12 -10
- data/app/services/mbeditor/file_operation_service.rb +38 -3
- data/app/services/mbeditor/file_tree_service.rb +30 -2
- data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
- data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
- data/app/services/mbeditor/git_diff_service.rb +5 -1
- data/app/services/mbeditor/git_info_service.rb +20 -8
- data/app/services/mbeditor/git_line_diff_service.rb +6 -2
- data/app/services/mbeditor/git_service.rb +65 -15
- data/app/services/mbeditor/js_definition_service.rb +3 -1
- data/app/services/mbeditor/js_globals_service.rb +18 -4
- data/app/services/mbeditor/js_members_service.rb +3 -2
- data/app/services/mbeditor/js_program_service.rb +15 -5
- data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
- data/app/services/mbeditor/process_runner.rb +42 -12
- data/app/services/mbeditor/ri_definition_service.rb +8 -1
- data/app/services/mbeditor/route_service.rb +45 -8
- data/app/services/mbeditor/rubocop_run_service.rb +98 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +73 -66
- data/app/services/mbeditor/search_replace_service.rb +42 -7
- data/app/services/mbeditor/test_runner_service.rb +45 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +4 -1
- data/lib/mbeditor/editor_bootstrap.rb +18 -12
- data/lib/mbeditor/engine.rb +22 -3
- data/lib/mbeditor/exception_log.rb +2 -2
- data/lib/mbeditor/pending_migrations.rb +33 -0
- data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
- data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +8 -2
|
@@ -6,6 +6,7 @@ var _React = React;
|
|
|
6
6
|
var useState = _React.useState;
|
|
7
7
|
var useEffect = _React.useEffect;
|
|
8
8
|
var useRef = _React.useRef;
|
|
9
|
+
var useMemo = _React.useMemo;
|
|
9
10
|
|
|
10
11
|
var IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp', 'bmp', 'avif'];
|
|
11
12
|
|
|
@@ -23,16 +24,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
23
24
|
var onContentChange = _ref.onContentChange;
|
|
24
25
|
var markers = _ref.markers;
|
|
25
26
|
var gitAvailable = _ref.gitAvailable === true;
|
|
26
|
-
var testAvailable = _ref.testAvailable === true;
|
|
27
27
|
var onFormat = _ref.onFormat;
|
|
28
28
|
var onSave = _ref.onSave;
|
|
29
|
-
var onRunTest = _ref.onRunTest;
|
|
30
29
|
var onRunTestAtCursor = _ref.onRunTestAtCursor;
|
|
31
30
|
var onShowHistory = _ref.onShowHistory;
|
|
32
31
|
var treeData = _ref.treeData || [];
|
|
33
32
|
var testResult = _ref.testResult;
|
|
34
33
|
var testPanelFile = _ref.testPanelFile;
|
|
35
|
-
var testLoading = _ref.testLoading;
|
|
36
34
|
var testInlineVisible = _ref.testInlineVisible;
|
|
37
35
|
var editorPrefs = _ref.editorPrefs || {};
|
|
38
36
|
var monacoReady = _ref.monacoReady !== false; // undefined means Monaco already loaded (legacy callers)
|
|
@@ -611,7 +609,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
611
609
|
var _histBranch = EditorStore.getState().gitBranch || '';
|
|
612
610
|
if (_histBranch) {
|
|
613
611
|
if (_reusingModel) {
|
|
614
|
-
HistoryService.resumeTracking(_histBranch, tab.path);
|
|
612
|
+
HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
|
|
615
613
|
} else {
|
|
616
614
|
HistoryService.beginTracking(_histBranch, tab.path, tab.content || '');
|
|
617
615
|
}
|
|
@@ -647,9 +645,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
647
645
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
648
646
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
649
647
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
// instead, gated on editorPrefs.indentOnPaste.
|
|
648
|
+
// Paste inserts what was copied, unchanged. See attachEditorFeatures for
|
|
649
|
+
// why re-indenting it was removed rather than repaired.
|
|
653
650
|
formatOnPaste: false,
|
|
654
651
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
655
652
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -1213,10 +1210,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1213
1210
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
1214
1211
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
1215
1212
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
formatOnPaste: false,
|
|
1213
|
+
// Paste inserts what was copied, unchanged. See attachEditorFeatures
|
|
1214
|
+
// for why re-indenting it was removed rather than repaired.
|
|
1215
|
+
formatOnPaste: false,
|
|
1220
1216
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1221
1217
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
1222
1218
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
@@ -1324,9 +1320,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1324
1320
|
var column = tab.gotoCol || 1;
|
|
1325
1321
|
var maxColumn = model.getLineMaxColumn(line);
|
|
1326
1322
|
if (column > maxColumn) column = maxColumn;
|
|
1323
|
+
// With both bounds the match itself is selected and the cursor ends up
|
|
1324
|
+
// just past it — where you'd start typing. With only gotoCol the
|
|
1325
|
+
// selection is empty, which is exactly setPosition.
|
|
1326
|
+
var endColumn = Math.min(tab.gotoEndCol || column, maxColumn);
|
|
1327
1327
|
|
|
1328
1328
|
editor.revealLineInCenter(line);
|
|
1329
|
-
|
|
1329
|
+
// setSelection(IRange) parks the cursor at the range's end, so no
|
|
1330
|
+
// setPosition afterwards — that would collapse the selection again.
|
|
1331
|
+
editor.setSelection({
|
|
1332
|
+
startLineNumber: line, startColumn: column,
|
|
1333
|
+
endLineNumber: line, endColumn: endColumn
|
|
1334
|
+
});
|
|
1330
1335
|
editor.focus();
|
|
1331
1336
|
|
|
1332
1337
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
@@ -1334,7 +1339,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1334
1339
|
}, 50);
|
|
1335
1340
|
|
|
1336
1341
|
return function () { clearTimeout(timer); };
|
|
1337
|
-
}, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
|
|
1342
|
+
}, [tab.gotoLine, tab.gotoCol, tab.gotoEndCol, tab.content, tab.loading]);
|
|
1338
1343
|
|
|
1339
1344
|
// Apply RuboCop markers
|
|
1340
1345
|
useEffect(function () {
|
|
@@ -1495,8 +1500,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1495
1500
|
gitLineRefreshRef.current = refresh;
|
|
1496
1501
|
|
|
1497
1502
|
// Immediate path for writes mbeditor made itself.
|
|
1503
|
+
//
|
|
1504
|
+
// Only when *this* file is one of the ones that changed: saving some other
|
|
1505
|
+
// file cannot move this file's diff against HEAD. Refreshing regardless
|
|
1506
|
+
// meant every open pane ran a git diff on every save anywhere in the
|
|
1507
|
+
// workspace, so the cost of a save scaled with how many editors were open.
|
|
1508
|
+
// A broadcast that names no paths still refreshes — that is the "something
|
|
1509
|
+
// happened, re-check everything" case (a commit, a branch switch), which
|
|
1510
|
+
// genuinely can change this file's diff without touching it.
|
|
1498
1511
|
var hasSocket = typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged;
|
|
1499
|
-
var onChanged = hasSocket ? function () {
|
|
1512
|
+
var onChanged = hasSocket ? function (payload) {
|
|
1513
|
+
if (payload && payload.paths && payload.paths.indexOf(tab.path) === -1) return;
|
|
1514
|
+
refresh();
|
|
1515
|
+
} : null;
|
|
1500
1516
|
if (onChanged) WebSocketService.onFilesChanged(onChanged);
|
|
1501
1517
|
|
|
1502
1518
|
return function () {
|
|
@@ -1572,6 +1588,16 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1572
1588
|
//
|
|
1573
1589
|
// Decorations, not view zones: a hint belongs on the line, and a zone would
|
|
1574
1590
|
// push the code around every time a file was opened.
|
|
1591
|
+
//
|
|
1592
|
+
// Redrawn whenever the text changes, not only when the file is opened. A
|
|
1593
|
+
// decoration is a zero-width range pinned at the end of the `def` line, and
|
|
1594
|
+
// Monaco keeps that position through an edit rather than re-deriving it — so
|
|
1595
|
+
// replacing a method (a paste over the selection is the usual way) left the
|
|
1596
|
+
// hint at a column that now falls in the middle of whatever text took its
|
|
1597
|
+
// place, rendering as `def sho GET /orders/:idw`. Nothing recomputed it,
|
|
1598
|
+
// so saving did not clear it either; it survived until the file was
|
|
1599
|
+
// reopened. The route data itself only depends on the route set, so an edit
|
|
1600
|
+
// redraws from the cached response without asking the server again.
|
|
1575
1601
|
useEffect(function () {
|
|
1576
1602
|
var editor = monacoRef.current;
|
|
1577
1603
|
if (!editor || !window.monaco) return;
|
|
@@ -1581,17 +1607,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1581
1607
|
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, []);
|
|
1582
1608
|
};
|
|
1583
1609
|
|
|
1584
|
-
if (
|
|
1610
|
+
if (editorPrefs.routeHints === false ||
|
|
1611
|
+
!tab.path || !/^app\/controllers\/.+_controller\.rb$/.test(tab.path)) {
|
|
1585
1612
|
clear();
|
|
1586
1613
|
return;
|
|
1587
1614
|
}
|
|
1588
1615
|
|
|
1589
1616
|
var cancelled = false;
|
|
1590
|
-
|
|
1617
|
+
var redrawTimer = null;
|
|
1618
|
+
|
|
1619
|
+
var draw = function (actions) {
|
|
1591
1620
|
if (cancelled) return;
|
|
1592
|
-
var actions = (data && data.actions) || {};
|
|
1593
1621
|
var model = editor.getModel();
|
|
1594
|
-
if (!model) return;
|
|
1622
|
+
if (!model || model.isDisposed()) return;
|
|
1595
1623
|
|
|
1596
1624
|
var decorations = [];
|
|
1597
1625
|
var text = model.getValue().split('\n');
|
|
@@ -1634,10 +1662,47 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1634
1662
|
});
|
|
1635
1663
|
|
|
1636
1664
|
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, decorations);
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
var contentSub = null;
|
|
1668
|
+
FileService.getRoutes(tab.path).then(function (data) {
|
|
1669
|
+
if (cancelled) return;
|
|
1670
|
+
var actions = (data && data.actions) || {};
|
|
1671
|
+
draw(actions);
|
|
1672
|
+
|
|
1673
|
+
// Debounced: a redraw per keystroke would rebuild every decoration in
|
|
1674
|
+
// the file for an edit that usually cannot have moved one.
|
|
1675
|
+
var model = editor.getModel();
|
|
1676
|
+
if (!model || !model.onDidChangeContent) return;
|
|
1677
|
+
contentSub = model.onDidChangeContent(function () {
|
|
1678
|
+
clearTimeout(redrawTimer);
|
|
1679
|
+
redrawTimer = setTimeout(function () { draw(actions); }, 250);
|
|
1680
|
+
});
|
|
1637
1681
|
}).catch(function () { /* hints are additive; a failure just means none */ });
|
|
1638
1682
|
|
|
1639
|
-
return function () {
|
|
1640
|
-
|
|
1683
|
+
return function () {
|
|
1684
|
+
cancelled = true;
|
|
1685
|
+
clearTimeout(redrawTimer);
|
|
1686
|
+
if (contentSub) contentSub.dispose();
|
|
1687
|
+
clear();
|
|
1688
|
+
};
|
|
1689
|
+
}, [tab.path, tab.externalContentVersion, monacoReady, editorPrefs.routeHints]);
|
|
1690
|
+
|
|
1691
|
+
// Blame and test view zones are anchored to line numbers, so they only need
|
|
1692
|
+
// rebuilding once an edit has settled. Both effects used to list tab.content
|
|
1693
|
+
// directly, which tore down and re-created every zone in the file on every
|
|
1694
|
+
// keystroke. This ticks 250 ms after the last change instead — the same
|
|
1695
|
+
// treatment the route-hint redraw above already gets.
|
|
1696
|
+
var _useStateZT = useState(0);
|
|
1697
|
+
var _useStateZT2 = _slicedToArray(_useStateZT, 2);
|
|
1698
|
+
var zoneTick = _useStateZT2[0];
|
|
1699
|
+
var setZoneTick = _useStateZT2[1];
|
|
1700
|
+
useEffect(function () {
|
|
1701
|
+
var timer = setTimeout(function () {
|
|
1702
|
+
setZoneTick(function (n) { return n + 1; });
|
|
1703
|
+
}, 250);
|
|
1704
|
+
return function () { clearTimeout(timer); };
|
|
1705
|
+
}, [tab.content]);
|
|
1641
1706
|
|
|
1642
1707
|
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
1643
1708
|
useEffect(function () {
|
|
@@ -1725,8 +1790,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1725
1790
|
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
1726
1791
|
}
|
|
1727
1792
|
|
|
1728
|
-
//
|
|
1729
|
-
}, [blameData, isBlameVisible, tab.id,
|
|
1793
|
+
// zoneTick, not tab.content: the zones follow the content, 250 ms behind it.
|
|
1794
|
+
}, [blameData, isBlameVisible, tab.id, zoneTick]);
|
|
1730
1795
|
|
|
1731
1796
|
// Check whether the current tab is the source file for the test that was run.
|
|
1732
1797
|
// e.g. testPanelFile = "test/controllers/theme_controller_test.rb"
|
|
@@ -1744,66 +1809,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1744
1809
|
return src === derived;
|
|
1745
1810
|
};
|
|
1746
1811
|
|
|
1747
|
-
var testFileCandidates = function(relativePath) {
|
|
1748
|
-
if (!relativePath || !relativePath.endsWith('.rb')) return [];
|
|
1749
|
-
|
|
1750
|
-
var basename = relativePath.slice(0, -3);
|
|
1751
|
-
var dirParts = relativePath.split('/');
|
|
1752
|
-
var leafName = basename.split('/').pop();
|
|
1753
|
-
var candidates = [];
|
|
1754
|
-
|
|
1755
|
-
if (dirParts[0] === 'app' && dirParts.length > 1) {
|
|
1756
|
-
var subPath = dirParts.slice(1).join('/');
|
|
1757
|
-
var subDir = subPath.indexOf('/') !== -1 ? subPath.slice(0, subPath.lastIndexOf('/')) : '';
|
|
1758
|
-
candidates.push('test/' + (subDir ? subDir + '/' : '') + leafName + '_test.rb');
|
|
1759
|
-
candidates.push('spec/' + (subDir ? subDir + '/' : '') + leafName + '_spec.rb');
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
if (dirParts[0] === 'lib') {
|
|
1763
|
-
var libSubPath = dirParts.slice(1).join('/');
|
|
1764
|
-
var libSubDir = libSubPath.indexOf('/') !== -1 ? libSubPath.slice(0, libSubPath.lastIndexOf('/')) : '';
|
|
1765
|
-
candidates.push('test/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1766
|
-
candidates.push('test/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1767
|
-
candidates.push('spec/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_spec.rb');
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
candidates.push('test/' + leafName + '_test.rb');
|
|
1771
|
-
candidates.push('spec/' + leafName + '_spec.rb');
|
|
1772
|
-
|
|
1773
|
-
return candidates.filter(function(candidate, index, list) {
|
|
1774
|
-
return list.indexOf(candidate) === index;
|
|
1775
|
-
});
|
|
1776
|
-
};
|
|
1777
|
-
|
|
1778
|
-
var treeHasPath = function(nodes, targetPath) {
|
|
1779
|
-
if (!targetPath) return false;
|
|
1780
|
-
var stack = (nodes || []).slice();
|
|
1781
|
-
|
|
1782
|
-
while (stack.length) {
|
|
1783
|
-
var node = stack.pop();
|
|
1784
|
-
if (!node) continue;
|
|
1785
|
-
if (node.path === targetPath) return true;
|
|
1786
|
-
if (node.children && node.children.length) {
|
|
1787
|
-
stack.push.apply(stack, node.children);
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
return false;
|
|
1792
|
-
};
|
|
1793
|
-
|
|
1794
|
-
var matchingTestFilePath = function(sourcePath) {
|
|
1795
|
-
var normalized = (sourcePath || '').replace(/^\/+/, '');
|
|
1796
|
-
if (!normalized || !normalized.endsWith('.rb')) return null;
|
|
1797
|
-
if (/^(test|spec)\//.test(normalized) && /_(test|spec)\.rb$/.test(normalized)) return normalized;
|
|
1798
|
-
|
|
1799
|
-
var candidates = testFileCandidates(normalized);
|
|
1800
|
-
for (var i = 0; i < candidates.length; i += 1) {
|
|
1801
|
-
if (treeHasPath(treeData, candidates[i])) return candidates[i];
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
return null;
|
|
1805
|
-
};
|
|
1806
|
-
|
|
1807
1812
|
// Map a test method name to the best-matching line in the source file.
|
|
1808
1813
|
// Extracts keywords from the test name and scores each source line.
|
|
1809
1814
|
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
@@ -1840,9 +1845,17 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1840
1845
|
var model = editor.getModel();
|
|
1841
1846
|
var lineCount = model ? model.getLineCount() : 0;
|
|
1842
1847
|
|
|
1843
|
-
|
|
1848
|
+
// Which test file a result entry belongs to. A whole-suite run spans many
|
|
1849
|
+
// of them and stamps each entry with its own `file`, so there is no single
|
|
1850
|
+
// testPanelFile to gate on; a per-file run leaves `file` unset on the
|
|
1851
|
+
// passing entries and testPanelFile is the answer for those.
|
|
1852
|
+
var testFileFor = function (t) { return (t && t.file) || testPanelFile; };
|
|
1853
|
+
var relevantTests = ((testResult && testResult.tests) || []).filter(function (t) {
|
|
1854
|
+
var file = testFileFor(t);
|
|
1855
|
+
return file && tab.path && isSourceForTest(tab.path, file);
|
|
1856
|
+
});
|
|
1844
1857
|
|
|
1845
|
-
if (!testResult || !testInlineVisible ||
|
|
1858
|
+
if (!testResult || !testInlineVisible || relevantTests.length === 0) {
|
|
1846
1859
|
clearTestZones(editor);
|
|
1847
1860
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1848
1861
|
return;
|
|
@@ -1853,10 +1866,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1853
1866
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1854
1867
|
|
|
1855
1868
|
var normPath = function(p) { return p ? p.replace(/^\/+/, '') : ''; };
|
|
1856
|
-
var viewingTestFile = normPath(tab.path) === normPath(testPanelFile);
|
|
1857
1869
|
|
|
1858
|
-
var
|
|
1859
|
-
var testsWithStatus = tests.filter(function (t) {
|
|
1870
|
+
var testsWithStatus = relevantTests.filter(function (t) {
|
|
1860
1871
|
return t.status === 'pass' || t.status === 'fail' || t.status === 'error';
|
|
1861
1872
|
});
|
|
1862
1873
|
|
|
@@ -1865,6 +1876,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1865
1876
|
// Determine line number for each test
|
|
1866
1877
|
var sourceContent = tab.content || '';
|
|
1867
1878
|
var mapped = testsWithStatus.map(function(t) {
|
|
1879
|
+
// Per entry, not per panel: in a suite run this tab is the test file
|
|
1880
|
+
// for some entries and the source counterpart for others.
|
|
1881
|
+
var viewingTestFile = normPath(tab.path) === normPath(testFileFor(t));
|
|
1868
1882
|
var line;
|
|
1869
1883
|
if (viewingTestFile && t.line && t.line >= 1 && t.line <= lineCount) {
|
|
1870
1884
|
line = t.line;
|
|
@@ -1923,8 +1937,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1923
1937
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1924
1938
|
}
|
|
1925
1939
|
|
|
1926
|
-
//
|
|
1927
|
-
|
|
1940
|
+
// zoneTick, not tab.content — see the blame effect. The effect still reads
|
|
1941
|
+
// tab.content for mapTestToSourceLine; the tick only decides when it re-runs.
|
|
1942
|
+
}, [testResult, testInlineVisible, testPanelFile, tab.id, tab.path, zoneTick]);
|
|
1928
1943
|
|
|
1929
1944
|
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
1930
1945
|
var markdownContent = tab.isPreview ? sourceTab && sourceTab.content || tab.content || '' : tab.content || '';
|
|
@@ -2253,18 +2268,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2253
2268
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2254
2269
|
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
|
|
2255
2270
|
),
|
|
2256
|
-
testAvailable && matchingTestFilePath(tab.path) && React.createElement(
|
|
2257
|
-
'button',
|
|
2258
|
-
{
|
|
2259
|
-
className: 'ide-icon-btn',
|
|
2260
|
-
onClick: function() { if (onRunTest) onRunTest(); },
|
|
2261
|
-
disabled: testLoading,
|
|
2262
|
-
'aria-busy': !!testLoading,
|
|
2263
|
-
title: 'Run Tests'
|
|
2264
|
-
},
|
|
2265
|
-
!testLoading && React.createElement('i', { className: 'fas fa-flask', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2266
|
-
!editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
|
|
2267
|
-
)
|
|
2268
2271
|
),
|
|
2269
2272
|
conflictCount > 0 && React.createElement(
|
|
2270
2273
|
'div', { className: 'mb-conflict-banner' },
|
|
@@ -2535,10 +2538,8 @@ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
|
|
|
2535
2538
|
prev.paneId === next.paneId &&
|
|
2536
2539
|
(prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
|
|
2537
2540
|
prev.gitAvailable === next.gitAvailable &&
|
|
2538
|
-
prev.testAvailable === next.testAvailable &&
|
|
2539
2541
|
prev.testResult === next.testResult &&
|
|
2540
2542
|
prev.testPanelFile === next.testPanelFile &&
|
|
2541
|
-
prev.testLoading === next.testLoading &&
|
|
2542
2543
|
prev.testInlineVisible === next.testInlineVisible &&
|
|
2543
2544
|
prev.editorPrefs === next.editorPrefs &&
|
|
2544
2545
|
prev.monacoReady === next.monacoReady;
|
|
@@ -423,7 +423,12 @@ var FileTree = function FileTree(_ref) {
|
|
|
423
423
|
return result;
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
// A per-level localeCompare sort of the whole visible tree, re-run on every
|
|
427
|
+
// render — including the ones caused by a git poll or a tab switch, neither
|
|
428
|
+
// of which can change the shape of the tree. Only these three inputs can.
|
|
429
|
+
var flatItems = useMemo(function () {
|
|
430
|
+
return flattenVisible(items || [], 0, '');
|
|
431
|
+
}, [items, expandedDirs, pendingCreate]);
|
|
427
432
|
flatItemsRef.current = flatItems;
|
|
428
433
|
|
|
429
434
|
var totalHeight = flatItems.length * ROW_HEIGHT;
|
|
@@ -516,4 +516,13 @@ var GitPanel = function GitPanel(_ref) {
|
|
|
516
516
|
);
|
|
517
517
|
};
|
|
518
518
|
|
|
519
|
-
|
|
519
|
+
// Same convention as FileTreeMemo: compare the data props only. The handlers
|
|
520
|
+
// are re-created on every MbeditorApp render but each one only calls a setState
|
|
521
|
+
// or a service, so they are stable in effect. Without this the panel — commit
|
|
522
|
+
// history, file lists, the Redmine card — re-rendered on every keystroke in the
|
|
523
|
+
// editor, because MbeditorApp is subscribed to the whole store.
|
|
524
|
+
window.GitPanel = React.memo(GitPanel, function (prev, next) {
|
|
525
|
+
return prev.gitInfo === next.gitInfo &&
|
|
526
|
+
prev.error === next.error &&
|
|
527
|
+
prev.redmineEnabled === next.redmineEnabled;
|
|
528
|
+
});
|
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Errors from the same pass render here too, so a drop that both conflicts
|
|
9
9
|
// and fails needs one dialog rather than two.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
var modalRef = React.useRef(null);
|
|
15
|
-
|
|
10
|
+
// Focus containment for a modal: initial focus in, Tab cycles inside, Escape
|
|
11
|
+
// dismisses, focus restored on unmount. Shared with ImportDialog — both are
|
|
12
|
+
// overlays over an editor that will happily steal focus back otherwise.
|
|
13
|
+
window.useModalFocusTrap = function useModalFocusTrap(modalRef, onDismiss) {
|
|
16
14
|
React.useEffect(function () {
|
|
17
15
|
var previouslyFocused = document.activeElement;
|
|
18
16
|
var modal = modalRef.current;
|
|
@@ -25,7 +23,7 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
|
|
|
25
23
|
if (e.key === 'Escape') {
|
|
26
24
|
e.preventDefault();
|
|
27
25
|
e.stopPropagation();
|
|
28
|
-
|
|
26
|
+
onDismiss();
|
|
29
27
|
return;
|
|
30
28
|
}
|
|
31
29
|
if (e.key !== 'Tab' || !modal) return;
|
|
@@ -57,7 +55,16 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
|
|
|
57
55
|
previouslyFocused.focus();
|
|
58
56
|
}
|
|
59
57
|
};
|
|
60
|
-
}, [
|
|
58
|
+
}, [onDismiss]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
var ImportConflictModal = function ImportConflictModal(_ref) {
|
|
62
|
+
var conflicts = _ref.conflicts || [];
|
|
63
|
+
var errors = _ref.errors || [];
|
|
64
|
+
var onResolve = _ref.onResolve;
|
|
65
|
+
var modalRef = React.useRef(null);
|
|
66
|
+
|
|
67
|
+
window.useModalFocusTrap(modalRef, React.useCallback(function () { onResolve('skip'); }, [onResolve]));
|
|
61
68
|
|
|
62
69
|
var button = function (label, mode, className) {
|
|
63
70
|
return React.createElement(
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Upload dialog: the pointed-at half of the drag-and-drop import.
|
|
4
|
+
//
|
|
5
|
+
// Everything past "pick files" is the existing import pipeline — the same
|
|
6
|
+
// entries shape, the same /import call, the same ImportConflictModal for
|
|
7
|
+
// replace/keep-both/skip. The only thing this adds is *where* the files land,
|
|
8
|
+
// which drag-and-drop answers by what you dropped onto and a file picker
|
|
9
|
+
// cannot answer at all.
|
|
10
|
+
//
|
|
11
|
+
// Destination suggestions come from the file tree already in memory
|
|
12
|
+
// (SearchService's index), so picking a folder that lives at
|
|
13
|
+
// app/assets/javascripts/ux/component offers that prefix rather than dumping
|
|
14
|
+
// `ux/component/` at the workspace root. The root stays on offer as the
|
|
15
|
+
// "create the structure from scratch" option.
|
|
16
|
+
var ImportDialog = function ImportDialog(_ref) {
|
|
17
|
+
var initialFolder = _ref.initialFolder || '';
|
|
18
|
+
var docs = _ref.docs || [];
|
|
19
|
+
var onCancel = _ref.onCancel;
|
|
20
|
+
var onImport = _ref.onImport;
|
|
21
|
+
|
|
22
|
+
var modalRef = React.useRef(null);
|
|
23
|
+
var filesInputRef = React.useRef(null);
|
|
24
|
+
var folderInputRef = React.useRef(null);
|
|
25
|
+
|
|
26
|
+
var _picked = React.useState(null);
|
|
27
|
+
var picked = _picked[0];
|
|
28
|
+
var setPicked = _picked[1];
|
|
29
|
+
|
|
30
|
+
var _dest = React.useState(initialFolder);
|
|
31
|
+
var dest = _dest[0];
|
|
32
|
+
var setDest = _dest[1];
|
|
33
|
+
|
|
34
|
+
window.useModalFocusTrap(modalRef, onCancel);
|
|
35
|
+
|
|
36
|
+
// webkitdirectory has no JSX/React prop, and React strips unknown lowercase
|
|
37
|
+
// attributes on <input>, so it is set on the DOM node directly.
|
|
38
|
+
React.useEffect(function () {
|
|
39
|
+
var el = folderInputRef.current;
|
|
40
|
+
if (!el) return;
|
|
41
|
+
el.setAttribute('webkitdirectory', '');
|
|
42
|
+
el.setAttribute('directory', '');
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
var entries = (picked && picked.entries) || [];
|
|
46
|
+
|
|
47
|
+
var suggestions = React.useMemo(function () {
|
|
48
|
+
if (entries.length === 0) return [];
|
|
49
|
+
return FileImport.suggestDestinations(entries, docs);
|
|
50
|
+
}, [picked, docs]);
|
|
51
|
+
|
|
52
|
+
var existingFiles = React.useMemo(function () {
|
|
53
|
+
var set = new Set();
|
|
54
|
+
docs.forEach(function (d) { if (d.type === 'file') set.add(d.path); });
|
|
55
|
+
return set;
|
|
56
|
+
}, [docs]);
|
|
57
|
+
|
|
58
|
+
var onPick = function (e) {
|
|
59
|
+
var result = FileImport.entriesFromFileList(e.target.files);
|
|
60
|
+
e.target.value = ''; // so picking the same folder twice still fires change
|
|
61
|
+
if (result.entries.length === 0) return;
|
|
62
|
+
setPicked(result);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
var normalisedDest = String(dest || '').trim().replace(/^\/+|\/+$/g, '');
|
|
66
|
+
|
|
67
|
+
var destOption = function (prefix, label, hint) {
|
|
68
|
+
var active = normalisedDest === prefix;
|
|
69
|
+
return React.createElement(
|
|
70
|
+
'button',
|
|
71
|
+
{
|
|
72
|
+
key: '$' + prefix,
|
|
73
|
+
type: 'button',
|
|
74
|
+
className: 'import-dest-option' + (active ? ' active' : ''),
|
|
75
|
+
onClick: function () { setDest(prefix); }
|
|
76
|
+
},
|
|
77
|
+
React.createElement('i', { className: (active ? 'fas fa-dot-circle' : 'far fa-circle') + ' import-dest-radio' }),
|
|
78
|
+
React.createElement('span', { className: 'import-dest-label' }, label),
|
|
79
|
+
hint && React.createElement('span', { className: 'import-dest-hint' }, hint)
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
var offered = {};
|
|
84
|
+
var options = [];
|
|
85
|
+
var offer = function (prefix, label, hint) {
|
|
86
|
+
if (offered['$' + prefix]) return;
|
|
87
|
+
offered['$' + prefix] = true;
|
|
88
|
+
options.push(destOption(prefix, label, hint));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// The folder this upload was started from is always first and always the
|
|
92
|
+
// active one. Offering it after the suggestions meant that once you had
|
|
93
|
+
// imported a folder anywhere, that same folder scored top of the suggestion
|
|
94
|
+
// list next time and sat at the head of the options — so the dialog read as
|
|
95
|
+
// "it remembered where I last put things". With nothing selected it was
|
|
96
|
+
// worse: the previous destination was the first option and the actually
|
|
97
|
+
// chosen one ("Workspace root") was last.
|
|
98
|
+
offer(initialFolder, initialFolder || 'Workspace root',
|
|
99
|
+
initialFolder ? 'selected folder' : 'create the folders from the root');
|
|
100
|
+
suggestions.forEach(function (s) {
|
|
101
|
+
var bits = [];
|
|
102
|
+
if (s.files) bits.push(s.files + ' matching file' + (s.files === 1 ? '' : 's'));
|
|
103
|
+
if (s.dirs) bits.push(s.dirs + ' matching folder' + (s.dirs === 1 ? '' : 's'));
|
|
104
|
+
offer(s.prefix, s.prefix, bits.join(', '));
|
|
105
|
+
});
|
|
106
|
+
offer('', 'Workspace root', 'create the folders from the root');
|
|
107
|
+
|
|
108
|
+
var targets = entries.map(function (e) {
|
|
109
|
+
return FileImport.joinPath(normalisedDest, e.relativePath);
|
|
110
|
+
});
|
|
111
|
+
var replacing = targets.filter(function (t) { return existingFiles.has(t); }).length;
|
|
112
|
+
|
|
113
|
+
return React.createElement(
|
|
114
|
+
'div',
|
|
115
|
+
{ className: 'schema-modal-overlay', onClick: onCancel },
|
|
116
|
+
React.createElement(
|
|
117
|
+
'div',
|
|
118
|
+
{
|
|
119
|
+
ref: modalRef,
|
|
120
|
+
className: 'schema-modal import-dialog',
|
|
121
|
+
role: 'dialog',
|
|
122
|
+
'aria-modal': 'true',
|
|
123
|
+
'aria-label': 'Upload files',
|
|
124
|
+
tabIndex: -1,
|
|
125
|
+
onClick: function (e) { e.stopPropagation(); }
|
|
126
|
+
},
|
|
127
|
+
React.createElement(
|
|
128
|
+
'div', { className: 'schema-modal-header' },
|
|
129
|
+
React.createElement('div', { className: 'schema-modal-title' },
|
|
130
|
+
React.createElement('i', { className: 'fas fa-upload', style: { marginRight: '8px', opacity: 0.7 } }),
|
|
131
|
+
'Upload files'),
|
|
132
|
+
React.createElement(
|
|
133
|
+
'button',
|
|
134
|
+
{ className: 'schema-modal-close', onClick: onCancel, title: 'Close' },
|
|
135
|
+
React.createElement('i', { className: 'fas fa-times' })
|
|
136
|
+
)
|
|
137
|
+
),
|
|
138
|
+
React.createElement(
|
|
139
|
+
'div', { className: 'schema-modal-body' },
|
|
140
|
+
|
|
141
|
+
React.createElement(
|
|
142
|
+
'div', { className: 'import-pick-row' },
|
|
143
|
+
React.createElement(
|
|
144
|
+
'button',
|
|
145
|
+
{ type: 'button', className: 'import-pick-btn',
|
|
146
|
+
onClick: function () { filesInputRef.current && filesInputRef.current.click(); } },
|
|
147
|
+
React.createElement('i', { className: 'far fa-file' }), ' Choose files…'
|
|
148
|
+
),
|
|
149
|
+
React.createElement(
|
|
150
|
+
'button',
|
|
151
|
+
{ type: 'button', className: 'import-pick-btn',
|
|
152
|
+
onClick: function () { folderInputRef.current && folderInputRef.current.click(); } },
|
|
153
|
+
React.createElement('i', { className: 'far fa-folder' }), ' Choose folder…'
|
|
154
|
+
),
|
|
155
|
+
React.createElement('input', {
|
|
156
|
+
ref: filesInputRef, type: 'file', multiple: true,
|
|
157
|
+
style: { display: 'none' }, onChange: onPick
|
|
158
|
+
}),
|
|
159
|
+
React.createElement('input', {
|
|
160
|
+
ref: folderInputRef, type: 'file', multiple: true,
|
|
161
|
+
style: { display: 'none' }, onChange: onPick
|
|
162
|
+
})
|
|
163
|
+
),
|
|
164
|
+
|
|
165
|
+
entries.length === 0 && React.createElement(
|
|
166
|
+
'div', { className: 'import-dialog-empty' },
|
|
167
|
+
'Pick files or a folder to upload. You can also drag them onto the explorer.'
|
|
168
|
+
),
|
|
169
|
+
|
|
170
|
+
picked && picked.truncated && React.createElement(
|
|
171
|
+
'div', { className: 'import-dialog-warning' },
|
|
172
|
+
'Only the first ' + FileImport.MAX_ENTRIES + ' files will be uploaded.'
|
|
173
|
+
),
|
|
174
|
+
|
|
175
|
+
entries.length > 0 && React.createElement(
|
|
176
|
+
React.Fragment, null,
|
|
177
|
+
React.createElement('div', { className: 'import-dialog-section-title' }, 'Destination'),
|
|
178
|
+
React.createElement('div', { className: 'import-dest-options' }, options),
|
|
179
|
+
React.createElement('input', {
|
|
180
|
+
className: 'import-dest-input',
|
|
181
|
+
type: 'text',
|
|
182
|
+
value: dest,
|
|
183
|
+
placeholder: 'workspace root',
|
|
184
|
+
spellCheck: false,
|
|
185
|
+
autoComplete: 'off',
|
|
186
|
+
'aria-label': 'Destination folder',
|
|
187
|
+
onChange: function (e) { setDest(e.target.value); }
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
React.createElement('div', { className: 'import-dialog-section-title' },
|
|
191
|
+
entries.length + ' file' + (entries.length === 1 ? '' : 's') +
|
|
192
|
+
(replacing > 0 ? ' — ' + replacing + ' will replace an existing file' : '')),
|
|
193
|
+
React.createElement(
|
|
194
|
+
'ul', { className: 'import-conflict-list' },
|
|
195
|
+
targets.map(function (t, i) {
|
|
196
|
+
return React.createElement(
|
|
197
|
+
'li', { key: t + '#' + i, title: t },
|
|
198
|
+
t,
|
|
199
|
+
existingFiles.has(t) && React.createElement(
|
|
200
|
+
'span', { className: 'import-target-replaces' }, 'replaces')
|
|
201
|
+
);
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
),
|
|
206
|
+
React.createElement(
|
|
207
|
+
'div', { className: 'import-conflict-actions' },
|
|
208
|
+
React.createElement('button', { className: 'import-conflict-btn', onClick: onCancel }, 'Cancel'),
|
|
209
|
+
React.createElement(
|
|
210
|
+
'button',
|
|
211
|
+
{
|
|
212
|
+
className: 'import-conflict-btn import-conflict-btn-primary',
|
|
213
|
+
disabled: entries.length === 0,
|
|
214
|
+
onClick: function () { onImport(entries, normalisedDest, picked); }
|
|
215
|
+
},
|
|
216
|
+
entries.length === 0 ? 'Upload' : 'Upload ' + entries.length + ' file' + (entries.length === 1 ? '' : 's')
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Expose globally for sprockets require
|
|
224
|
+
window.ImportDialog = ImportDialog;
|