mbeditor 0.12.9 → 0.13.0
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 +150 -0
- data/app/assets/javascripts/mbeditor/application.js +7 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
- 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 +102 -25
- 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 +216 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +598 -130
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +162 -6
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +427 -111
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +122 -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 +30 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +268 -53
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- 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 +173 -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 +7 -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 +7 -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 +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +65 -64
- data/app/services/mbeditor/search_replace_service.rb +31 -5
- data/app/services/mbeditor/test_runner_service.rb +98 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +12 -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 +2 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- metadata +7 -2
|
@@ -682,6 +682,13 @@ var ModelGraph = (function () {
|
|
|
682
682
|
var centreRef = React.useRef(centreOn);
|
|
683
683
|
centreRef.current = centreOn;
|
|
684
684
|
|
|
685
|
+
// Same reason as centreRef: MbeditorApp passes a fresh closure on every one
|
|
686
|
+
// of its renders, and listing onOpenModel in the scene memo's deps rebuilt
|
|
687
|
+
// all ~10,000 SVG elements each time — once per keystroke, whenever this
|
|
688
|
+
// tab happened to be open.
|
|
689
|
+
var openModelRef = React.useRef(onOpenModel);
|
|
690
|
+
openModelRef.current = onOpenModel;
|
|
691
|
+
|
|
685
692
|
var fitRef = React.useRef(fitToPane);
|
|
686
693
|
fitRef.current = fitToPane;
|
|
687
694
|
|
|
@@ -762,7 +769,6 @@ var ModelGraph = (function () {
|
|
|
762
769
|
var b = placed.positions[e.to];
|
|
763
770
|
if (!a || !b || e.from === e.to) return null;
|
|
764
771
|
var d = edgePath(a, b);
|
|
765
|
-
var isHovered = hovered && hovered.index === i;
|
|
766
772
|
return React.createElement(
|
|
767
773
|
'g',
|
|
768
774
|
// Tagged so a node hover can find its own edges with one DOM query
|
|
@@ -774,16 +780,29 @@ var ModelGraph = (function () {
|
|
|
774
780
|
React.createElement('path', {
|
|
775
781
|
d: d,
|
|
776
782
|
className: 'mg-edge-hit',
|
|
777
|
-
|
|
783
|
+
// The lit class goes on the DOM node directly, the way
|
|
784
|
+
// enterModel/litRef do. `hovered` is React state read from
|
|
785
|
+
// inside this memo, and it is deliberately not a dep — putting
|
|
786
|
+
// it there would rebuild the whole scene on every edge hover —
|
|
787
|
+
// so a className derived from it never updated at all.
|
|
788
|
+
onMouseEnter: function (ev) {
|
|
789
|
+
var line = ev.currentTarget.nextElementSibling;
|
|
790
|
+
if (line) line.classList.add('mg-edge-hovered');
|
|
791
|
+
setHovered({ index: i, edge: e });
|
|
792
|
+
},
|
|
778
793
|
onMouseMove: function (ev) {
|
|
779
794
|
var rect = svgRef.current.getBoundingClientRect();
|
|
780
795
|
setPointer({ x: ev.clientX - rect.left, y: ev.clientY - rect.top });
|
|
781
796
|
},
|
|
782
|
-
onMouseLeave: function () {
|
|
797
|
+
onMouseLeave: function (ev) {
|
|
798
|
+
var line = ev.currentTarget.nextElementSibling;
|
|
799
|
+
if (line) line.classList.remove('mg-edge-hovered');
|
|
800
|
+
setHovered(null);
|
|
801
|
+
}
|
|
783
802
|
}),
|
|
784
803
|
React.createElement('path', {
|
|
785
804
|
d: d,
|
|
786
|
-
className: 'mg-edge ' + (MACRO_CLASS[e.macro] || '')
|
|
805
|
+
className: 'mg-edge ' + (MACRO_CLASS[e.macro] || ''),
|
|
787
806
|
markerEnd: 'url(#mg-arrow)'
|
|
788
807
|
})
|
|
789
808
|
);
|
|
@@ -831,7 +850,7 @@ var ModelGraph = (function () {
|
|
|
831
850
|
onClick: function (ev) {
|
|
832
851
|
ev.stopPropagation();
|
|
833
852
|
if (dragRef.current && dragRef.current.moved) return;
|
|
834
|
-
if (
|
|
853
|
+
if (openModelRef.current) openModelRef.current(m);
|
|
835
854
|
}
|
|
836
855
|
},
|
|
837
856
|
React.createElement('title', null, 'Open the full schema for ' + m.name),
|
|
@@ -868,7 +887,7 @@ var ModelGraph = (function () {
|
|
|
868
887
|
);
|
|
869
888
|
})
|
|
870
889
|
];
|
|
871
|
-
}, [placed, graph, focused
|
|
890
|
+
}, [placed, graph, focused]);
|
|
872
891
|
|
|
873
892
|
if (loading) {
|
|
874
893
|
return React.createElement('div', { className: 'ide-model-graph-empty' }, 'Building the model graph…');
|
|
@@ -23,6 +23,13 @@ var ProblemsPanel = (function () {
|
|
|
23
23
|
info: 'fa-info-circle'
|
|
24
24
|
};
|
|
25
25
|
var SEVERITY_LABEL = { error: 'Error', warning: 'Warning', info: 'Info' };
|
|
26
|
+
// RuboCop's own severity names (the /rubocop JSON carries strings, not the
|
|
27
|
+
// numeric Monaco severities the markers use).
|
|
28
|
+
var COP_SEVERITY_KIND = {
|
|
29
|
+
fatal: 'error', error: 'error', warning: 'warning',
|
|
30
|
+
convention: 'info', refactor: 'info', info: 'info'
|
|
31
|
+
};
|
|
32
|
+
|
|
26
33
|
// Long enough to recognise the statement, short enough not to push the
|
|
27
34
|
// location off the end of the row.
|
|
28
35
|
var CODE_PREVIEW_LIMIT = 120;
|
|
@@ -82,9 +89,24 @@ var ProblemsPanel = (function () {
|
|
|
82
89
|
|
|
83
90
|
// The status bar deliberately tallies only errors and warnings: a count that
|
|
84
91
|
// included every convention offense would be a number nobody acts on.
|
|
92
|
+
//
|
|
93
|
+
// Same scope as collect(), none of its work: the full walk sorts every marker
|
|
94
|
+
// and reads a source line per marker to build previews the caller then throws
|
|
95
|
+
// away, and it runs on every marker change — for a JS file, every keystroke.
|
|
85
96
|
function counts() {
|
|
86
|
-
var
|
|
87
|
-
return { errors:
|
|
97
|
+
var monaco = window.monaco;
|
|
98
|
+
if (!monaco || !monaco.editor) return { errors: 0, warnings: 0 };
|
|
99
|
+
|
|
100
|
+
var errors = 0;
|
|
101
|
+
var warnings = 0;
|
|
102
|
+
monaco.editor.getModels().forEach(function (model) {
|
|
103
|
+
if (!model._mbeditorPath || model.isDisposed()) return;
|
|
104
|
+
monaco.editor.getModelMarkers({ resource: model.uri }).forEach(function (m) {
|
|
105
|
+
if (m.severity === SEVERITY_ERROR) errors += 1;
|
|
106
|
+
else if (m.severity === SEVERITY_WARNING) warnings += 1;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return { errors: errors, warnings: warnings };
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
var Panel = function ProblemsPanelComponent(_ref) {
|
|
@@ -156,6 +178,47 @@ var ProblemsPanel = (function () {
|
|
|
156
178
|
if (FileService.clearExceptions) FileService.clearExceptions()["catch"](function () {});
|
|
157
179
|
};
|
|
158
180
|
|
|
181
|
+
// Whole-workspace rubocop. Separate from `problems` above: markers only
|
|
182
|
+
// exist for models Monaco has loaded, so this is the only view that can
|
|
183
|
+
// see a file nobody has opened.
|
|
184
|
+
var _workspace = React.useState(null);
|
|
185
|
+
var workspace = _workspace[0], setWorkspace = _workspace[1];
|
|
186
|
+
var _running = React.useState(null);
|
|
187
|
+
var running = _running[0], setRunning = _running[1];
|
|
188
|
+
|
|
189
|
+
var runRubocop = function (mode) {
|
|
190
|
+
if (running) return;
|
|
191
|
+
setRunning(mode);
|
|
192
|
+
FileService.runRubocop(mode)
|
|
193
|
+
.then(function (data) { setWorkspace(data); })
|
|
194
|
+
["catch"](function (e) {
|
|
195
|
+
var res = e && e.response && e.response.data;
|
|
196
|
+
setWorkspace(res || { ok: false, error: (e && e.message) || 'RuboCop failed', files: [] });
|
|
197
|
+
})
|
|
198
|
+
.then(function () { setRunning(null); });
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// The workspace list is a snapshot of one run against one working tree.
|
|
202
|
+
// After a checkout it describes files that may no longer have those
|
|
203
|
+
// offenses — or no longer exist — so it is dropped rather than left to
|
|
204
|
+
// look current.
|
|
205
|
+
React.useEffect(function () {
|
|
206
|
+
var onBranchChanged = function () { setWorkspace(null); };
|
|
207
|
+
window.addEventListener('mbeditor:branch-changed', onBranchChanged);
|
|
208
|
+
return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
var openAllOffending = function () {
|
|
212
|
+
if (!workspace || !onOpenFile) return;
|
|
213
|
+
var files = workspace.files || [];
|
|
214
|
+
// Each open is a Monaco model plus a tab; a default-config run over a
|
|
215
|
+
// real app returns hundreds, which would wedge the editor silently.
|
|
216
|
+
if (files.length > 20 && !window.confirm('Open ' + files.length + ' files?')) return;
|
|
217
|
+
files.forEach(function (f) {
|
|
218
|
+
onOpenFile(f.path, (f.offenses[0] && f.offenses[0].line) || 1, 1);
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
159
222
|
var MIN_HEIGHT = 120;
|
|
160
223
|
var _height = React.useState(function () {
|
|
161
224
|
var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
|
|
@@ -186,10 +249,22 @@ var ProblemsPanel = (function () {
|
|
|
186
249
|
|
|
187
250
|
React.useEffect(function () {
|
|
188
251
|
if (!window.monaco || !window.monaco.editor) return;
|
|
189
|
-
|
|
252
|
+
// Debounced for the same reason the status-bar tally is: markers change
|
|
253
|
+
// on every keystroke in a JS file, and collect() walks every model.
|
|
254
|
+
var timer = null;
|
|
255
|
+
var refresh = function () {
|
|
256
|
+
if (timer) clearTimeout(timer);
|
|
257
|
+
timer = setTimeout(function () {
|
|
258
|
+
timer = null;
|
|
259
|
+
setProblems(collect());
|
|
260
|
+
}, 250);
|
|
261
|
+
};
|
|
190
262
|
var sub = window.monaco.editor.onDidChangeMarkers(refresh);
|
|
191
|
-
|
|
192
|
-
return function () {
|
|
263
|
+
setProblems(collect());
|
|
264
|
+
return function () {
|
|
265
|
+
if (timer) clearTimeout(timer);
|
|
266
|
+
sub.dispose();
|
|
267
|
+
};
|
|
193
268
|
}, []);
|
|
194
269
|
|
|
195
270
|
var needle = filter.trim().toLowerCase();
|
|
@@ -249,6 +324,40 @@ var ProblemsPanel = (function () {
|
|
|
249
324
|
);
|
|
250
325
|
})
|
|
251
326
|
),
|
|
327
|
+
React.createElement(
|
|
328
|
+
'div',
|
|
329
|
+
{ className: 'ide-problems-actions' },
|
|
330
|
+
React.createElement('button', {
|
|
331
|
+
type: 'button', className: 'ide-problems-btn',
|
|
332
|
+
disabled: !!running,
|
|
333
|
+
title: 'Run rubocop over the whole workspace',
|
|
334
|
+
onClick: function () { runRubocop('check'); }
|
|
335
|
+
},
|
|
336
|
+
React.createElement('i', { className: running === 'check' ? 'fas fa-spinner fa-spin' : 'fas fa-play' }),
|
|
337
|
+
React.createElement('span', null, ' RuboCop')),
|
|
338
|
+
React.createElement('button', {
|
|
339
|
+
type: 'button', className: 'ide-problems-btn',
|
|
340
|
+
// Nothing to correct until a run says so, and rerunning `-a` on a
|
|
341
|
+
// clean tree is a slow no-op.
|
|
342
|
+
disabled: !!running || !workspace || !workspace.correctable,
|
|
343
|
+
title: workspace && workspace.correctable
|
|
344
|
+
? 'Safe-autocorrect ' + workspace.correctable + ' offense(s) and rerun'
|
|
345
|
+
: 'Run RuboCop first',
|
|
346
|
+
onClick: function () { runRubocop('autocorrect'); }
|
|
347
|
+
},
|
|
348
|
+
React.createElement('i', { className: running === 'autocorrect' ? 'fas fa-spinner fa-spin' : 'fas fa-magic' }),
|
|
349
|
+
React.createElement('span', null, ' Fix')),
|
|
350
|
+
React.createElement('button', {
|
|
351
|
+
type: 'button', className: 'ide-problems-btn',
|
|
352
|
+
disabled: !workspace || !(workspace.files || []).length,
|
|
353
|
+
title: workspace && (workspace.files || []).length
|
|
354
|
+
? 'Open all ' + workspace.files.length + ' file(s) with offenses'
|
|
355
|
+
: 'Run RuboCop first',
|
|
356
|
+
onClick: openAllOffending
|
|
357
|
+
},
|
|
358
|
+
React.createElement('i', { className: 'fas fa-folder-open' }),
|
|
359
|
+
React.createElement('span', null, ' Open all'))
|
|
360
|
+
),
|
|
252
361
|
React.createElement('input', {
|
|
253
362
|
className: 'ide-problems-filter',
|
|
254
363
|
type: 'text',
|
|
@@ -264,6 +373,48 @@ var ProblemsPanel = (function () {
|
|
|
264
373
|
React.createElement(
|
|
265
374
|
'div',
|
|
266
375
|
{ className: 'ide-problems-body' },
|
|
376
|
+
workspace && React.createElement(
|
|
377
|
+
'div',
|
|
378
|
+
{ className: 'ide-problems-workspace' },
|
|
379
|
+
workspace.error
|
|
380
|
+
? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop: ' + workspace.error)
|
|
381
|
+
: (workspace.files || []).length === 0
|
|
382
|
+
? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop found no offenses in the workspace')
|
|
383
|
+
: workspace.files.map(function (file) {
|
|
384
|
+
return React.createElement(
|
|
385
|
+
'div',
|
|
386
|
+
{ className: 'ide-problems-file', key: 'ws:' + file.path },
|
|
387
|
+
React.createElement(
|
|
388
|
+
'div',
|
|
389
|
+
{ className: 'ide-problems-file-name' },
|
|
390
|
+
React.createElement('i', { className: 'fas fa-gavel', 'aria-hidden': 'true' }),
|
|
391
|
+
' ' + file.path,
|
|
392
|
+
React.createElement('span', { className: 'ide-problems-file-count' }, file.offenses.length)
|
|
393
|
+
),
|
|
394
|
+
file.offenses.map(function (o, i) {
|
|
395
|
+
var kind = COP_SEVERITY_KIND[o.severity] || 'info';
|
|
396
|
+
return React.createElement(
|
|
397
|
+
'button',
|
|
398
|
+
{
|
|
399
|
+
type: 'button',
|
|
400
|
+
className: 'ide-problems-item ide-problems-item-' + kind,
|
|
401
|
+
key: 'ws:' + file.path + ':' + i,
|
|
402
|
+
title: o.message + '\n' + file.path + ':' + o.line,
|
|
403
|
+
'aria-label': SEVERITY_LABEL[kind] + ': ' + o.message + ', ' + file.path + ' line ' + o.line,
|
|
404
|
+
onClick: function () { if (onOpenFile) onOpenFile(file.path, o.line, o.column); }
|
|
405
|
+
},
|
|
406
|
+
React.createElement('i', {
|
|
407
|
+
className: 'fas ' + SEVERITY_ICON[kind] + ' ide-problems-icon', 'aria-hidden': 'true'
|
|
408
|
+
}),
|
|
409
|
+
React.createElement('span', { className: 'ide-problems-msg' }, o.message),
|
|
410
|
+
React.createElement('span', { className: 'ide-problems-source' }, o.copName),
|
|
411
|
+
o.correctable && React.createElement('span', { className: 'ide-problems-source' }, 'fixable'),
|
|
412
|
+
React.createElement('span', { className: 'ide-problems-loc' }, '[' + o.line + ', ' + o.column + ']')
|
|
413
|
+
);
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
})
|
|
417
|
+
),
|
|
267
418
|
exceptions.length > 0 && React.createElement(
|
|
268
419
|
'div',
|
|
269
420
|
{ className: 'ide-problems-file ide-problems-exceptions' },
|
|
@@ -302,6 +453,7 @@ var ProblemsPanel = (function () {
|
|
|
302
453
|
type: 'button',
|
|
303
454
|
className: 'ide-problems-item ide-problems-frame',
|
|
304
455
|
key: ex.id + ':' + i,
|
|
456
|
+
title: 'Open ' + f.file + ':' + f.line,
|
|
305
457
|
'aria-label': 'Open ' + f.file + ' line ' + f.line,
|
|
306
458
|
onClick: function () { if (onOpenFile) onOpenFile(f.file, f.line, 1); }
|
|
307
459
|
},
|
|
@@ -312,7 +464,7 @@ var ProblemsPanel = (function () {
|
|
|
312
464
|
);
|
|
313
465
|
})
|
|
314
466
|
),
|
|
315
|
-
total === 0 && exceptions.length > 0
|
|
467
|
+
total === 0 && (exceptions.length > 0 || workspace)
|
|
316
468
|
? null
|
|
317
469
|
: total === 0
|
|
318
470
|
? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
|
|
@@ -340,6 +492,10 @@ var ProblemsPanel = (function () {
|
|
|
340
492
|
type: 'button',
|
|
341
493
|
className: 'ide-problems-item ide-problems-item-' + kind,
|
|
342
494
|
key: entry.path + ':' + marker.startLineNumber + ':' + index,
|
|
495
|
+
// The message ellipsises in a short panel, so the full
|
|
496
|
+
// text is on hover as well as on the label.
|
|
497
|
+
title: marker.message + '\n' + entry.path + ':' + marker.startLineNumber +
|
|
498
|
+
(item.code ? ' — ' + item.code : ''),
|
|
343
499
|
'aria-label': SEVERITY_LABEL[kind] + ': ' + marker.message +
|
|
344
500
|
', ' + entry.path + ' line ' + marker.startLineNumber +
|
|
345
501
|
(item.code ? ', source: ' + item.code : ''),
|
|
@@ -139,39 +139,45 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
139
139
|
setSelectedIndex(0);
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
142
|
+
// Debounced: a MiniSearch query plus a linear pass and a sort over every
|
|
143
|
+
// path in the workspace ran on every keystroke, between the keypress and
|
|
144
|
+
// the character appearing.
|
|
145
|
+
var timer = setTimeout(function () {
|
|
146
|
+
var res = SearchService.searchFiles(query);
|
|
147
|
+
// Filter by type: always include files; include dirs only when showFolders is on
|
|
148
|
+
var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
|
|
149
|
+
// Sort, in order of precedence:
|
|
150
|
+
// 1. match quality — exact basename > prefix > substring > other, so a
|
|
151
|
+
// worse match can never jump the queue however recently it was opened
|
|
152
|
+
// 2. how recently the file was opened, most recent first
|
|
153
|
+
// 3. the static file-type tier (controller > model > … > noise)
|
|
154
|
+
//
|
|
155
|
+
// Recency sits above the type tier deliberately: when two files match a
|
|
156
|
+
// query equally well, the one you were just working in is almost always
|
|
157
|
+
// the one you meant, and that beats a guess made from the directory name.
|
|
158
|
+
// Files never opened all tie here and fall through to the type tier, which
|
|
159
|
+
// is what orders the bulk of a cold result list.
|
|
160
|
+
//
|
|
161
|
+
// Directories keep their +100 penalty inside the type tier so files still
|
|
162
|
+
// come first. JS sort is stable in modern engines, so MiniSearch's own
|
|
163
|
+
// relevance order remains the final tiebreaker.
|
|
164
|
+
var ranks = recentRanks();
|
|
165
|
+
var NEVER_OPENED = Infinity;
|
|
166
|
+
filtered.sort(function(a, b) {
|
|
167
|
+
var aRelevance = getMatchRelevance(a, query);
|
|
168
|
+
var bRelevance = getMatchRelevance(b, query);
|
|
169
|
+
if (aRelevance !== bRelevance) return aRelevance - bRelevance;
|
|
170
|
+
var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
|
|
171
|
+
var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
|
|
172
|
+
if (aRecent !== bRecent) return aRecent - bRecent;
|
|
173
|
+
var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
|
|
174
|
+
var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
|
|
175
|
+
return aPriority - bPriority;
|
|
176
|
+
});
|
|
177
|
+
setResults(filtered.slice(0, 200));
|
|
178
|
+
setSelectedIndex(0);
|
|
179
|
+
}, 120);
|
|
180
|
+
return function () { clearTimeout(timer); };
|
|
175
181
|
}, [query, showFolders]);
|
|
176
182
|
|
|
177
183
|
// ── Keyboard ─────────────────────────────────────────────────────────────
|
|
@@ -22,6 +22,7 @@ var TabBar = function TabBar(_ref) {
|
|
|
22
22
|
var onCloseSaved = _ref.onCloseSaved;
|
|
23
23
|
var onCloseAll = _ref.onCloseAll;
|
|
24
24
|
var onNewFile = _ref.onNewFile;
|
|
25
|
+
var markers = _ref.markers || {};
|
|
25
26
|
var tabDisplayMode = _ref.tabDisplayMode || 'scroll';
|
|
26
27
|
|
|
27
28
|
var containerRef = useRef(null);
|
|
@@ -51,7 +52,7 @@ var TabBar = function TabBar(_ref) {
|
|
|
51
52
|
var setDropTargetSide = _useState8[1];
|
|
52
53
|
|
|
53
54
|
var getTabMarkerClass = function getTabMarkerClass(tab) {
|
|
54
|
-
var tabMarkers = tab.
|
|
55
|
+
var tabMarkers = markers[tab.id] || [];
|
|
55
56
|
if (!tabMarkers.length) return '';
|
|
56
57
|
|
|
57
58
|
var hasError = tabMarkers.some(function (marker) {
|
|
@@ -79,7 +80,10 @@ var TabBar = function TabBar(_ref) {
|
|
|
79
80
|
activeEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
+
// tabs.length, not tabs: the array is fresh on every content change, so
|
|
84
|
+
// depending on it fired a smooth scroll per keystroke. Only opening, closing
|
|
85
|
+
// or switching a tab can move the active one out of view.
|
|
86
|
+
}, [activeId, tabs.length]);
|
|
83
87
|
|
|
84
88
|
// Close context menu on outside click (bubble phase so onMouseDown on the menu can stop it)
|
|
85
89
|
useEffect(function () {
|
|
@@ -189,6 +193,11 @@ var TabBar = function TabBar(_ref) {
|
|
|
189
193
|
'div',
|
|
190
194
|
{
|
|
191
195
|
className: 'tab-close',
|
|
196
|
+
// No role="button" here: the host app's Pico CSS skins
|
|
197
|
+
// [role=button] with a primary background and form padding, which
|
|
198
|
+
// turns this into a blue square. It is a plain div with a tooltip,
|
|
199
|
+
// as it was — Ctrl+W is the keyboard path.
|
|
200
|
+
title: 'Close ' + tab.name + (tab.dirty ? ' (unsaved changes)' : ''),
|
|
192
201
|
onClick: function (e) {
|
|
193
202
|
e.stopPropagation();
|
|
194
203
|
onClose(tab.id);
|