mbeditor 0.9.0 → 0.10.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 +95 -0
- data/README.md +4 -79
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +186 -16
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +50 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +99 -20
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +217 -0
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +34 -3
- data/app/assets/javascripts/mbeditor/editor_plugins.js +106 -52
- data/app/assets/javascripts/mbeditor/git_service.js +8 -0
- data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
- data/app/assets/stylesheets/mbeditor/editor.css +176 -4
- data/app/assets/stylesheets/mbeditor/themes.css +35 -0
- data/app/controllers/mbeditor/editors_controller.rb +33 -4
- data/app/controllers/mbeditor/git_controller.rb +11 -0
- data/app/services/mbeditor/git_line_diff_service.rb +99 -0
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +5 -2
|
@@ -440,6 +440,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
440
440
|
var showLogPanel = _useStateLog2[0];
|
|
441
441
|
var setShowLogPanel = _useStateLog2[1];
|
|
442
442
|
|
|
443
|
+
var _useStateProblems = useState(false);
|
|
444
|
+
var _useStateProblems2 = _slicedToArray(_useStateProblems, 2);
|
|
445
|
+
var showProblemsPanel = _useStateProblems2[0];
|
|
446
|
+
var setShowProblemsPanel = _useStateProblems2[1];
|
|
447
|
+
|
|
448
|
+
// Error/warning tallies across the open tabs, mirrored into the status bar.
|
|
449
|
+
var _useStateProblemCounts = useState({ errors: 0, warnings: 0 });
|
|
450
|
+
var _useStateProblemCounts2 = _slicedToArray(_useStateProblemCounts, 2);
|
|
451
|
+
var problemCounts = _useStateProblemCounts2[0];
|
|
452
|
+
var setProblemCounts = _useStateProblemCounts2[1];
|
|
453
|
+
|
|
443
454
|
var _useState18g = useState(320);
|
|
444
455
|
var _useState18g2 = _slicedToArray(_useState18g, 2);
|
|
445
456
|
var gitPanelWidth = _useState18g2[0];
|
|
@@ -1487,14 +1498,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1487
1498
|
});
|
|
1488
1499
|
}
|
|
1489
1500
|
|
|
1490
|
-
// Auto-refresh the file tree every 10s to pick up external changes (new files,
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1501
|
+
// Auto-refresh the file tree every 10s to pick up external changes (new files,
|
|
1502
|
+
// deletions, a branch switch in a terminal).
|
|
1503
|
+
//
|
|
1504
|
+
// This runs whether or not the WebSocket is connected. It used to skip when
|
|
1505
|
+
// connected, on the reasoning that the push covered it — but the server only
|
|
1506
|
+
// broadcasts from mbeditor's own mutation endpoints, so a connected socket
|
|
1507
|
+
// meant external changes were never picked up at all. The push remains the
|
|
1508
|
+
// instant path for our own writes; this is what catches everything else.
|
|
1493
1509
|
// Uses functional setTreeData to skip the re-render when nothing has changed.
|
|
1494
1510
|
useEffect(function () {
|
|
1495
1511
|
var intervalId = setInterval(function () {
|
|
1496
1512
|
if (document.hidden) return;
|
|
1497
|
-
if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
|
|
1498
1513
|
FileService.getTree().then(function (data) {
|
|
1499
1514
|
var newData = data || [];
|
|
1500
1515
|
setTreeData(function (prevData) {
|
|
@@ -1536,6 +1551,31 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1536
1551
|
};
|
|
1537
1552
|
}, []);
|
|
1538
1553
|
|
|
1554
|
+
// Keep the status-bar error/warning tallies in step with Monaco's markers.
|
|
1555
|
+
// Every diagnostic source in the editor — rubocop, ruby-lsp, the TypeScript
|
|
1556
|
+
// worker — lands here, so one subscription covers all of them. Markers change
|
|
1557
|
+
// on every keystroke for JS, so the recount is debounced.
|
|
1558
|
+
useEffect(function () {
|
|
1559
|
+
if (!monacoReady || !window.monaco || !window.monaco.editor || !window.ProblemsPanel) return;
|
|
1560
|
+
|
|
1561
|
+
var timer = null;
|
|
1562
|
+
var recount = function () {
|
|
1563
|
+
if (timer) clearTimeout(timer);
|
|
1564
|
+
timer = setTimeout(function () {
|
|
1565
|
+
timer = null;
|
|
1566
|
+
setProblemCounts(window.ProblemsPanel.counts());
|
|
1567
|
+
}, 250);
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
var sub = window.monaco.editor.onDidChangeMarkers(recount);
|
|
1571
|
+
recount();
|
|
1572
|
+
|
|
1573
|
+
return function () {
|
|
1574
|
+
if (timer) clearTimeout(timer);
|
|
1575
|
+
sub.dispose();
|
|
1576
|
+
};
|
|
1577
|
+
}, [monacoReady]);
|
|
1578
|
+
|
|
1539
1579
|
var handleSelectFile = function handleSelectFile(path, name, line, col) {
|
|
1540
1580
|
TabManager.openTab(path, name, line, null, false, col);
|
|
1541
1581
|
handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
|
|
@@ -2747,6 +2787,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2747
2787
|
setShowLogPanel(function (prev) { return !prev; });
|
|
2748
2788
|
};
|
|
2749
2789
|
|
|
2790
|
+
var toggleProblemsPanel = function toggleProblemsPanel() {
|
|
2791
|
+
setShowProblemsPanel(function (prev) { return !prev; });
|
|
2792
|
+
};
|
|
2793
|
+
|
|
2750
2794
|
var toggleZenMode = function toggleZenMode() {
|
|
2751
2795
|
setZenMode(function (prev) {
|
|
2752
2796
|
var next = !prev;
|
|
@@ -3382,16 +3426,24 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3382
3426
|
"Mini Browser Editor — ",
|
|
3383
3427
|
window.location.host
|
|
3384
3428
|
),
|
|
3429
|
+
// The slot claims all the room between the title and the buttons; the
|
|
3430
|
+
// search pill then takes 75% of it, centred. Sizing the pill against a
|
|
3431
|
+
// slot rather than against the title bar keeps it proportional to the
|
|
3432
|
+
// actual gap as the toolbar's button labels grow and shrink.
|
|
3385
3433
|
React.createElement(
|
|
3386
|
-
'
|
|
3387
|
-
{
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3434
|
+
'div',
|
|
3435
|
+
{ className: 'ide-titlebar-search-slot' },
|
|
3436
|
+
React.createElement(
|
|
3437
|
+
'button',
|
|
3438
|
+
{
|
|
3439
|
+
type: 'button',
|
|
3440
|
+
className: 'ide-titlebar-search',
|
|
3441
|
+
title: 'Search files (Ctrl/Cmd+P)',
|
|
3442
|
+
onClick: function () { setQuickOpen(true); }
|
|
3443
|
+
},
|
|
3444
|
+
React.createElement('i', { className: 'fas fa-search', style: { marginRight: '6px', opacity: 0.7 } }),
|
|
3445
|
+
React.createElement('span', { className: 'ide-titlebar-search-text' }, 'Search files…')
|
|
3446
|
+
)
|
|
3395
3447
|
),
|
|
3396
3448
|
React.createElement(
|
|
3397
3449
|
"div",
|
|
@@ -3455,13 +3507,6 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3455
3507
|
)
|
|
3456
3508
|
),
|
|
3457
3509
|
React.createElement("div", { className: "statusbar-sep" }),
|
|
3458
|
-
React.createElement(
|
|
3459
|
-
"button",
|
|
3460
|
-
{ type: "button", className: "statusbar-btn", onClick: toggleLogPanel, title: "Toggle Rails log (Ctrl+Shift+L)" },
|
|
3461
|
-
React.createElement("i", { className: "fas fa-stream" }),
|
|
3462
|
-
!editorPrefs.toolbarIconOnly && " Logs"
|
|
3463
|
-
),
|
|
3464
|
-
React.createElement("div", { className: "statusbar-sep" }),
|
|
3465
3510
|
React.createElement(
|
|
3466
3511
|
"button",
|
|
3467
3512
|
{ type: "button", className: "statusbar-btn", onClick: function () { return setShowHelp(true); }, title: "Keyboard shortcuts & help" },
|
|
@@ -4974,6 +5019,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4974
5019
|
),
|
|
4975
5020
|
showLogPanel && !zenMode && React.createElement(window.LogPanel || LogPanel, {
|
|
4976
5021
|
onClose: function () { setShowLogPanel(false); }
|
|
5022
|
+
}),
|
|
5023
|
+
showProblemsPanel && !zenMode && React.createElement(window.ProblemsPanel || ProblemsPanel, {
|
|
5024
|
+
onClose: function () { setShowProblemsPanel(false); },
|
|
5025
|
+
onOpenFile: function (path, line, col) {
|
|
5026
|
+
handleSelectFile(path, path.split('/').pop(), line, col);
|
|
5027
|
+
}
|
|
4977
5028
|
})
|
|
4978
5029
|
),
|
|
4979
5030
|
React.createElement(
|
|
@@ -4998,6 +5049,23 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4998
5049
|
state.gitInfo.behind
|
|
4999
5050
|
)
|
|
5000
5051
|
),
|
|
5052
|
+
React.createElement(
|
|
5053
|
+
"button",
|
|
5054
|
+
{
|
|
5055
|
+
type: "button",
|
|
5056
|
+
className: "statusbar-btn statusbar-problems" + (showProblemsPanel ? " active" : ""),
|
|
5057
|
+
onClick: toggleProblemsPanel,
|
|
5058
|
+
title: problemCounts.errors + " error(s), " + problemCounts.warnings +
|
|
5059
|
+
" warning(s) in open files — click to open Problems"
|
|
5060
|
+
},
|
|
5061
|
+
React.createElement("i", { className: "fas fa-bug statusbar-problems-error-icon" }),
|
|
5062
|
+
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.errors),
|
|
5063
|
+
React.createElement("i", {
|
|
5064
|
+
className: "fas fa-exclamation-triangle statusbar-problems-warning-icon",
|
|
5065
|
+
style: { marginLeft: "8px" }
|
|
5066
|
+
}),
|
|
5067
|
+
React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
|
|
5068
|
+
),
|
|
5001
5069
|
!serverOnline && (function () {
|
|
5002
5070
|
var dirtyCount = state.panes.reduce(function (acc, p) {
|
|
5003
5071
|
return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
|
|
@@ -5027,6 +5095,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5027
5095
|
{ className: "statusbar-msg " + state.statusMessage.kind },
|
|
5028
5096
|
state.statusMessage.text
|
|
5029
5097
|
),
|
|
5098
|
+
React.createElement(
|
|
5099
|
+
"button",
|
|
5100
|
+
{
|
|
5101
|
+
type: "button",
|
|
5102
|
+
className: "statusbar-btn statusbar-logs-btn" + (showLogPanel ? " active" : ""),
|
|
5103
|
+
onClick: toggleLogPanel,
|
|
5104
|
+
title: "Toggle Rails log (Ctrl+Shift+L)"
|
|
5105
|
+
},
|
|
5106
|
+
React.createElement("i", { className: "fas fa-stream" }),
|
|
5107
|
+
" Logs"
|
|
5108
|
+
),
|
|
5030
5109
|
activeEOL && React.createElement(
|
|
5031
5110
|
"button",
|
|
5032
5111
|
{
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ProblemsPanel — bottom drawer listing the diagnostics Monaco is holding for
|
|
4
|
+
// the files you have open: rubocop and ruby-lsp for Ruby, the TypeScript
|
|
5
|
+
// worker's syntax errors for JS/JSX, and anything else that writes markers.
|
|
6
|
+
//
|
|
7
|
+
// Scope is deliberately "open tabs", not the workspace: markers only exist for
|
|
8
|
+
// models Monaco has loaded, so a count over anything wider would be a lie.
|
|
9
|
+
// Closing a tab drops its problems, same as VS Code with an unopened file.
|
|
10
|
+
var ProblemsPanel = (function () {
|
|
11
|
+
var SEVERITY_ERROR = 8;
|
|
12
|
+
var SEVERITY_WARNING = 4;
|
|
13
|
+
// Long enough to recognise the statement, short enough not to push the
|
|
14
|
+
// location off the end of the row.
|
|
15
|
+
var CODE_PREVIEW_LIMIT = 120;
|
|
16
|
+
|
|
17
|
+
// The offending source line, trimmed, for display beside the message. Markers
|
|
18
|
+
// can outlive the edit that invalidated them by a frame, so a line number
|
|
19
|
+
// past the end of the buffer yields nothing rather than throwing.
|
|
20
|
+
function codePreview(model, lineNumber) {
|
|
21
|
+
if (!lineNumber || lineNumber < 1 || lineNumber > model.getLineCount()) return '';
|
|
22
|
+
|
|
23
|
+
var text = model.getLineContent(lineNumber).trim();
|
|
24
|
+
return text.length > CODE_PREVIEW_LIMIT ? text.slice(0, CODE_PREVIEW_LIMIT) + '…' : text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Reading markers means walking every model, so callers that only want the
|
|
28
|
+
// counts share this one pass. Exposed on the component for the status bar.
|
|
29
|
+
function collect() {
|
|
30
|
+
var monaco = window.monaco;
|
|
31
|
+
if (!monaco || !monaco.editor) return { errors: [], warnings: [], byFile: [] };
|
|
32
|
+
|
|
33
|
+
var errors = [];
|
|
34
|
+
var warnings = [];
|
|
35
|
+
var byFile = [];
|
|
36
|
+
|
|
37
|
+
monaco.editor.getModels().forEach(function (model) {
|
|
38
|
+
var path = model._mbeditorPath;
|
|
39
|
+
if (!path || model.isDisposed()) return;
|
|
40
|
+
|
|
41
|
+
var markers = monaco.editor.getModelMarkers({ resource: model.uri }).filter(function (m) {
|
|
42
|
+
return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING;
|
|
43
|
+
});
|
|
44
|
+
if (markers.length === 0) return;
|
|
45
|
+
|
|
46
|
+
markers.sort(function (a, b) { return a.startLineNumber - b.startLineNumber; });
|
|
47
|
+
|
|
48
|
+
// Carry the source line alongside the marker: read here, while the model
|
|
49
|
+
// is in hand, rather than looking the model up again at render time.
|
|
50
|
+
var entries = markers.map(function (m) {
|
|
51
|
+
(m.severity === SEVERITY_ERROR ? errors : warnings).push(m);
|
|
52
|
+
return { marker: m, code: codePreview(model, m.startLineNumber) };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
byFile.push({ path: path, markers: entries });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
byFile.sort(function (a, b) { return a.path < b.path ? -1 : a.path > b.path ? 1 : 0; });
|
|
59
|
+
return { errors: errors, warnings: warnings, byFile: byFile };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function counts() {
|
|
63
|
+
var all = collect();
|
|
64
|
+
return { errors: all.errors.length, warnings: all.warnings.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var Panel = function ProblemsPanelComponent(_ref) {
|
|
68
|
+
var onClose = _ref.onClose;
|
|
69
|
+
var onOpenFile = _ref.onOpenFile;
|
|
70
|
+
|
|
71
|
+
var _problems = React.useState(collect);
|
|
72
|
+
var problems = _problems[0], setProblems = _problems[1];
|
|
73
|
+
var _filter = React.useState('');
|
|
74
|
+
var filter = _filter[0], setFilter = _filter[1];
|
|
75
|
+
|
|
76
|
+
var MIN_HEIGHT = 120;
|
|
77
|
+
var _height = React.useState(function () {
|
|
78
|
+
var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
|
|
79
|
+
return (saved && saved >= MIN_HEIGHT) ? saved : 240;
|
|
80
|
+
});
|
|
81
|
+
var height = _height[0], setHeight = _height[1];
|
|
82
|
+
var heightRef = React.useRef(height);
|
|
83
|
+
heightRef.current = height;
|
|
84
|
+
|
|
85
|
+
// Same delta-based resize as the log drawer.
|
|
86
|
+
var onResizeMouseDown = function (e) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
var startY = e.clientY;
|
|
89
|
+
var startHeight = heightRef.current;
|
|
90
|
+
var onMove = function (ev) {
|
|
91
|
+
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
92
|
+
var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
|
|
93
|
+
setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
|
|
94
|
+
};
|
|
95
|
+
var onUp = function () {
|
|
96
|
+
document.removeEventListener('mousemove', onMove);
|
|
97
|
+
document.removeEventListener('mouseup', onUp);
|
|
98
|
+
window.localStorage.setItem('mbeditorProblemsHeight', String(heightRef.current));
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener('mousemove', onMove);
|
|
101
|
+
document.addEventListener('mouseup', onUp);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
React.useEffect(function () {
|
|
105
|
+
if (!window.monaco || !window.monaco.editor) return;
|
|
106
|
+
var refresh = function () { setProblems(collect()); };
|
|
107
|
+
var sub = window.monaco.editor.onDidChangeMarkers(refresh);
|
|
108
|
+
refresh();
|
|
109
|
+
return function () { sub.dispose(); };
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
var needle = filter.trim().toLowerCase();
|
|
113
|
+
var shown = needle
|
|
114
|
+
? problems.byFile.map(function (entry) {
|
|
115
|
+
return {
|
|
116
|
+
path: entry.path,
|
|
117
|
+
markers: entry.markers.filter(function (item) {
|
|
118
|
+
return (item.marker.message + ' ' + item.code + ' ' + entry.path)
|
|
119
|
+
.toLowerCase().indexOf(needle) !== -1;
|
|
120
|
+
})
|
|
121
|
+
};
|
|
122
|
+
}).filter(function (entry) { return entry.markers.length > 0; })
|
|
123
|
+
: problems.byFile;
|
|
124
|
+
|
|
125
|
+
var total = problems.errors.length + problems.warnings.length;
|
|
126
|
+
|
|
127
|
+
return React.createElement(
|
|
128
|
+
'div',
|
|
129
|
+
{ className: 'ide-problems-drawer', style: { height: height + 'px' } },
|
|
130
|
+
React.createElement('div', {
|
|
131
|
+
className: 'ide-problems-resize',
|
|
132
|
+
title: 'Drag to resize',
|
|
133
|
+
onMouseDown: onResizeMouseDown
|
|
134
|
+
}),
|
|
135
|
+
React.createElement(
|
|
136
|
+
'div',
|
|
137
|
+
{ className: 'ide-problems-header' },
|
|
138
|
+
React.createElement('i', { className: 'fas fa-bug' }),
|
|
139
|
+
React.createElement('span', { className: 'ide-problems-title' }, 'Problems'),
|
|
140
|
+
React.createElement(
|
|
141
|
+
'span',
|
|
142
|
+
{ className: 'ide-problems-summary' },
|
|
143
|
+
problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
|
|
144
|
+
', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
|
|
145
|
+
' in open files'
|
|
146
|
+
),
|
|
147
|
+
React.createElement('input', {
|
|
148
|
+
className: 'ide-problems-filter',
|
|
149
|
+
type: 'text',
|
|
150
|
+
placeholder: 'Filter…',
|
|
151
|
+
value: filter,
|
|
152
|
+
onChange: function (e) { setFilter(e.target.value); }
|
|
153
|
+
}),
|
|
154
|
+
React.createElement('button', {
|
|
155
|
+
type: 'button', className: 'ide-problems-btn',
|
|
156
|
+
title: 'Close', onClick: onClose
|
|
157
|
+
}, React.createElement('i', { className: 'fas fa-times' }))
|
|
158
|
+
),
|
|
159
|
+
React.createElement(
|
|
160
|
+
'div',
|
|
161
|
+
{ className: 'ide-problems-body' },
|
|
162
|
+
total === 0
|
|
163
|
+
? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
|
|
164
|
+
: shown.length === 0
|
|
165
|
+
? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems match the filter')
|
|
166
|
+
: shown.map(function (entry) {
|
|
167
|
+
return React.createElement(
|
|
168
|
+
'div',
|
|
169
|
+
{ className: 'ide-problems-file', key: entry.path },
|
|
170
|
+
React.createElement(
|
|
171
|
+
'div',
|
|
172
|
+
{ className: 'ide-problems-file-name' },
|
|
173
|
+
entry.path,
|
|
174
|
+
React.createElement('span', { className: 'ide-problems-file-count' }, entry.markers.length)
|
|
175
|
+
),
|
|
176
|
+
entry.markers.map(function (item, index) {
|
|
177
|
+
var marker = item.marker;
|
|
178
|
+
var isError = marker.severity === SEVERITY_ERROR;
|
|
179
|
+
return React.createElement(
|
|
180
|
+
'button',
|
|
181
|
+
{
|
|
182
|
+
type: 'button',
|
|
183
|
+
className: 'ide-problems-item ide-problems-item-' + (isError ? 'error' : 'warning'),
|
|
184
|
+
key: entry.path + ':' + marker.startLineNumber + ':' + index,
|
|
185
|
+
'aria-label': (isError ? 'Error' : 'Warning') + ': ' + marker.message +
|
|
186
|
+
', ' + entry.path + ' line ' + marker.startLineNumber +
|
|
187
|
+
(item.code ? ', source: ' + item.code : ''),
|
|
188
|
+
onClick: function () {
|
|
189
|
+
if (onOpenFile) onOpenFile(entry.path, marker.startLineNumber, marker.startColumn);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
React.createElement('i', {
|
|
193
|
+
className: 'fas ' + (isError ? 'fa-bug' : 'fa-exclamation-triangle') + ' ide-problems-icon',
|
|
194
|
+
'aria-hidden': 'true'
|
|
195
|
+
}),
|
|
196
|
+
React.createElement('span', { className: 'ide-problems-msg' }, marker.message),
|
|
197
|
+
item.code && React.createElement('code', { className: 'ide-problems-code' }, item.code),
|
|
198
|
+
marker.source && React.createElement('span', { className: 'ide-problems-source' }, marker.source),
|
|
199
|
+
React.createElement(
|
|
200
|
+
'span',
|
|
201
|
+
{ className: 'ide-problems-loc' },
|
|
202
|
+
'[' + marker.startLineNumber + ', ' + marker.startColumn + ']'
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
Panel.collect = collect;
|
|
213
|
+
Panel.counts = counts;
|
|
214
|
+
return Panel;
|
|
215
|
+
})();
|
|
216
|
+
|
|
217
|
+
window.ProblemsPanel = ProblemsPanel;
|
|
@@ -94,6 +94,20 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
94
94
|
return 50;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Recently opened files, most recent first, as a path -> rank lookup. Built
|
|
98
|
+
// once per query rather than per comparison, since the sort calls this for
|
|
99
|
+
// every pair.
|
|
100
|
+
function recentRanks() {
|
|
101
|
+
var ranks = {};
|
|
102
|
+
var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
|
|
103
|
+
? TabManager.getRecentFiles() : [];
|
|
104
|
+
recent.forEach(function (entry, index) {
|
|
105
|
+
var path = typeof entry === 'string' ? entry : (entry && entry.path);
|
|
106
|
+
if (path && !(path in ranks)) ranks[path] = index;
|
|
107
|
+
});
|
|
108
|
+
return ranks;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
// Match relevance within a priority tier: exact basename > prefix > substring > other.
|
|
98
112
|
function getMatchRelevance(result, q) {
|
|
99
113
|
if (!q) return 3;
|
|
@@ -128,13 +142,30 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
128
142
|
var res = SearchService.searchFiles(query);
|
|
129
143
|
// Filter by type: always include files; include dirs only when showFolders is on
|
|
130
144
|
var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
|
|
131
|
-
// Sort
|
|
132
|
-
//
|
|
133
|
-
//
|
|
145
|
+
// Sort, in order of precedence:
|
|
146
|
+
// 1. match quality — exact basename > prefix > substring > other, so a
|
|
147
|
+
// worse match can never jump the queue however recently it was opened
|
|
148
|
+
// 2. how recently the file was opened, most recent first
|
|
149
|
+
// 3. the static file-type tier (controller > model > … > noise)
|
|
150
|
+
//
|
|
151
|
+
// Recency sits above the type tier deliberately: when two files match a
|
|
152
|
+
// query equally well, the one you were just working in is almost always
|
|
153
|
+
// the one you meant, and that beats a guess made from the directory name.
|
|
154
|
+
// Files never opened all tie here and fall through to the type tier, which
|
|
155
|
+
// is what orders the bulk of a cold result list.
|
|
156
|
+
//
|
|
157
|
+
// Directories keep their +100 penalty inside the type tier so files still
|
|
158
|
+
// come first. JS sort is stable in modern engines, so MiniSearch's own
|
|
159
|
+
// relevance order remains the final tiebreaker.
|
|
160
|
+
var ranks = recentRanks();
|
|
161
|
+
var NEVER_OPENED = Infinity;
|
|
134
162
|
filtered.sort(function(a, b) {
|
|
135
163
|
var aRelevance = getMatchRelevance(a, query);
|
|
136
164
|
var bRelevance = getMatchRelevance(b, query);
|
|
137
165
|
if (aRelevance !== bRelevance) return aRelevance - bRelevance;
|
|
166
|
+
var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
|
|
167
|
+
var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
|
|
168
|
+
if (aRecent !== bRecent) return aRecent - bRecent;
|
|
138
169
|
var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
|
|
139
170
|
var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
|
|
140
171
|
return aPriority - bPriority;
|