mbeditor 0.13.0 → 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 +66 -1
- data/app/assets/javascripts/mbeditor/application.js +0 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +22 -2
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +11 -87
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +171 -205
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +35 -7
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +97 -75
- data/app/assets/javascripts/mbeditor/editor_plugins.js +7 -1
- data/app/assets/javascripts/mbeditor/file_service.js +8 -18
- data/app/assets/javascripts/mbeditor/search_service.js +20 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +7 -4
- data/app/assets/stylesheets/mbeditor/editor.css +138 -67
- data/app/channels/mbeditor/collaboration_channel.rb +8 -2
- data/app/controllers/mbeditor/editors_controller.rb +8 -26
- data/app/services/mbeditor/collaboration_doc_store.rb +42 -3
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- data/app/services/mbeditor/js_globals_service.rb +12 -1
- data/app/services/mbeditor/rubocop_run_service.rb +17 -5
- data/app/services/mbeditor/schema_service.rb +8 -2
- data/app/services/mbeditor/search_replace_service.rb +11 -2
- data/app/services/mbeditor/test_runner_service.rb +3 -56
- data/lib/mbeditor/configuration.rb +0 -8
- data/lib/mbeditor/route_map.rb +0 -1
- data/lib/mbeditor/version.rb +1 -1
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +4 -3
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
|
@@ -112,6 +112,10 @@ var ProblemsPanel = (function () {
|
|
|
112
112
|
var Panel = function ProblemsPanelComponent(_ref) {
|
|
113
113
|
var onClose = _ref.onClose;
|
|
114
114
|
var onOpenFile = _ref.onOpenFile;
|
|
115
|
+
// Called after `-a` has rewritten files on disk, so the app can re-read
|
|
116
|
+
// every open tab. Nothing else would: the write came from a subprocess,
|
|
117
|
+
// not from the editor, so the buffers and their markers are stale.
|
|
118
|
+
var onFilesRewritten = _ref.onFilesRewritten;
|
|
115
119
|
|
|
116
120
|
var _problems = React.useState(collect);
|
|
117
121
|
var problems = _problems[0], setProblems = _problems[1];
|
|
@@ -190,7 +194,10 @@ var ProblemsPanel = (function () {
|
|
|
190
194
|
if (running) return;
|
|
191
195
|
setRunning(mode);
|
|
192
196
|
FileService.runRubocop(mode)
|
|
193
|
-
.then(function (data) {
|
|
197
|
+
.then(function (data) {
|
|
198
|
+
setWorkspace(data);
|
|
199
|
+
if (mode === 'autocorrect' && data && data.ok && onFilesRewritten) onFilesRewritten();
|
|
200
|
+
})
|
|
194
201
|
["catch"](function (e) {
|
|
195
202
|
var res = e && e.response && e.response.data;
|
|
196
203
|
setWorkspace(res || { ok: false, error: (e && e.message) || 'RuboCop failed', files: [] });
|
|
@@ -283,6 +290,25 @@ var ProblemsPanel = (function () {
|
|
|
283
290
|
}).filter(function (entry) { return entry.markers.length > 0; })
|
|
284
291
|
: problems.byFile;
|
|
285
292
|
|
|
293
|
+
// The workspace run and the marker set are different scopes, but they render
|
|
294
|
+
// in one panel under one set of severity chips. Counting only the markers
|
|
295
|
+
// made the header disagree with its own body: click RuboCop, get 400
|
|
296
|
+
// offenses listed under chips reading 0 / 0 / 0, and hiding "warning" left
|
|
297
|
+
// every workspace warning on screen. The chips describe what the panel is
|
|
298
|
+
// showing, so they have to cover both — and the filter has to reach both.
|
|
299
|
+
var wsFiles = (workspace && workspace.files) || [];
|
|
300
|
+
var wsCounts = { error: 0, warning: 0, info: 0 };
|
|
301
|
+
var wsShown = [];
|
|
302
|
+
wsFiles.forEach(function (file) {
|
|
303
|
+
var kept = [];
|
|
304
|
+
file.offenses.forEach(function (o) {
|
|
305
|
+
var kind = COP_SEVERITY_KIND[o.severity] || 'info';
|
|
306
|
+
wsCounts[kind] += 1;
|
|
307
|
+
if (severities[kind]) kept.push(o);
|
|
308
|
+
});
|
|
309
|
+
if (kept.length) wsShown.push({ path: file.path, offenses: kept });
|
|
310
|
+
});
|
|
311
|
+
|
|
286
312
|
var total = problems.errors.length + problems.warnings.length + problems.infos.length;
|
|
287
313
|
|
|
288
314
|
return React.createElement(
|
|
@@ -304,9 +330,9 @@ var ProblemsPanel = (function () {
|
|
|
304
330
|
'div',
|
|
305
331
|
{ className: 'ide-problems-severity-filter' },
|
|
306
332
|
[
|
|
307
|
-
{ kind: 'error', count: problems.errors.length },
|
|
308
|
-
{ kind: 'warning', count: problems.warnings.length },
|
|
309
|
-
{ kind: 'info', count: problems.infos.length }
|
|
333
|
+
{ kind: 'error', count: problems.errors.length + wsCounts.error },
|
|
334
|
+
{ kind: 'warning', count: problems.warnings.length + wsCounts.warning },
|
|
335
|
+
{ kind: 'info', count: problems.infos.length + wsCounts.info }
|
|
310
336
|
].map(function (s) {
|
|
311
337
|
var on = severities[s.kind];
|
|
312
338
|
return React.createElement(
|
|
@@ -342,7 +368,9 @@ var ProblemsPanel = (function () {
|
|
|
342
368
|
disabled: !!running || !workspace || !workspace.correctable,
|
|
343
369
|
title: workspace && workspace.correctable
|
|
344
370
|
? 'Safe-autocorrect ' + workspace.correctable + ' offense(s) and rerun'
|
|
345
|
-
:
|
|
371
|
+
: workspace
|
|
372
|
+
? 'Nothing left that safe autocorrect can fix'
|
|
373
|
+
: 'Run RuboCop first',
|
|
346
374
|
onClick: function () { runRubocop('autocorrect'); }
|
|
347
375
|
},
|
|
348
376
|
React.createElement('i', { className: running === 'autocorrect' ? 'fas fa-spinner fa-spin' : 'fas fa-magic' }),
|
|
@@ -378,9 +406,9 @@ var ProblemsPanel = (function () {
|
|
|
378
406
|
{ className: 'ide-problems-workspace' },
|
|
379
407
|
workspace.error
|
|
380
408
|
? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop: ' + workspace.error)
|
|
381
|
-
:
|
|
409
|
+
: wsFiles.length === 0
|
|
382
410
|
? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop found no offenses in the workspace')
|
|
383
|
-
:
|
|
411
|
+
: wsShown.map(function (file) {
|
|
384
412
|
return React.createElement(
|
|
385
413
|
'div',
|
|
386
414
|
{ className: 'ide-problems-file', key: 'ws:' + file.path },
|
|
@@ -23,6 +23,100 @@ function saveFavourites(list) {
|
|
|
23
23
|
try { localStorage.setItem(FAVS_KEY, JSON.stringify(list)); } catch (e) {}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// ── Ranking ────────────────────────────────────────────────────────────────
|
|
27
|
+
// Module scope, not component scope, so `rankResults` can be exercised without
|
|
28
|
+
// rendering — see script/check-quick-open-ranking.mjs.
|
|
29
|
+
|
|
30
|
+
// Priority tier for a file path: lower = shown first.
|
|
31
|
+
// Order: controller > model > helper > concern > view > job > other > noise
|
|
32
|
+
function getFilePriority(path) {
|
|
33
|
+
var p = (path || '').toLowerCase();
|
|
34
|
+
if (p.indexOf('/controllers/') >= 0) return 1;
|
|
35
|
+
if (p.indexOf('/models/') >= 0) return 2;
|
|
36
|
+
if (p.indexOf('/helpers/') >= 0) return 3;
|
|
37
|
+
if (p.indexOf('/concerns/') >= 0) return 4;
|
|
38
|
+
if (p.indexOf('/views/') >= 0) return 5;
|
|
39
|
+
if (p.indexOf('/jobs/') >= 0) return 6;
|
|
40
|
+
// Deprioritise: migrations, schema, compiled assets, vendor, lock files
|
|
41
|
+
if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
|
|
42
|
+
if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
|
|
43
|
+
if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
|
|
44
|
+
p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
|
|
45
|
+
return 50;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Recently opened files, most recent first, as a path -> rank lookup. Built
|
|
49
|
+
// once per query rather than per comparison, since the sort calls this for
|
|
50
|
+
// every pair.
|
|
51
|
+
function recentRanks() {
|
|
52
|
+
var ranks = {};
|
|
53
|
+
var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
|
|
54
|
+
? TabManager.getRecentFiles() : [];
|
|
55
|
+
recent.forEach(function (entry, index) {
|
|
56
|
+
var path = typeof entry === 'string' ? entry : (entry && entry.path);
|
|
57
|
+
if (path && !(path in ranks)) ranks[path] = index;
|
|
58
|
+
});
|
|
59
|
+
return ranks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Match relevance within a priority tier:
|
|
63
|
+
// exact basename > basename prefix > basename substring > path substring > fuzzy-only.
|
|
64
|
+
// FUZZY_ONLY is the bucket everything MiniSearch matched approximately lands
|
|
65
|
+
// in — a typo'd or transposed query hits nothing literally, so an exact match
|
|
66
|
+
// can never be demoted by loosening the index search.
|
|
67
|
+
var FUZZY_ONLY = 4;
|
|
68
|
+
function getMatchRelevance(result, q) {
|
|
69
|
+
if (!q) return FUZZY_ONLY;
|
|
70
|
+
var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
|
|
71
|
+
var lq = q.toLowerCase();
|
|
72
|
+
if (name === lq) return 0;
|
|
73
|
+
if (name.slice(0, lq.length) === lq) return 1;
|
|
74
|
+
if (name.indexOf(lq) >= 0) return 2;
|
|
75
|
+
if ((result.path || '').toLowerCase().indexOf(lq) >= 0) return 3;
|
|
76
|
+
return FUZZY_ONLY;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Filter SearchService hits by type and order them for display.
|
|
80
|
+
// Precedence:
|
|
81
|
+
// 1. files before folders — a folder is never what Ctrl+P is for, and as a
|
|
82
|
+
// tier below match quality an exactly-named folder used to outrank every
|
|
83
|
+
// file that matched
|
|
84
|
+
// 2. match quality (see getMatchRelevance), so a worse match can never jump
|
|
85
|
+
// the queue however recently it was opened
|
|
86
|
+
// 3. MiniSearch's own score, but only within the fuzzy-only bucket: nothing
|
|
87
|
+
// there matched literally, so edit distance is the only signal for which
|
|
88
|
+
// near-miss the user meant
|
|
89
|
+
// 4. how recently the file was opened, most recent first
|
|
90
|
+
// 5. the static file-type tier (controller > model > … > noise)
|
|
91
|
+
//
|
|
92
|
+
// Recency sits above the type tier deliberately: when two files match a query
|
|
93
|
+
// equally well, the one you were just working in is almost always the one you
|
|
94
|
+
// meant, and that beats a guess made from the directory name. Files never
|
|
95
|
+
// opened all tie here and fall through to the type tier, which is what orders
|
|
96
|
+
// the bulk of a cold result list.
|
|
97
|
+
//
|
|
98
|
+
// JS sort is stable in modern engines, so MiniSearch's own relevance order
|
|
99
|
+
// remains the final tiebreaker.
|
|
100
|
+
function rankResults(res, query, showFolders) {
|
|
101
|
+
var filtered = showFolders ? res.slice() : res.filter(function (r) { return r.type !== 'dir'; });
|
|
102
|
+
var ranks = recentRanks();
|
|
103
|
+
var NEVER_OPENED = Infinity;
|
|
104
|
+
filtered.sort(function (a, b) {
|
|
105
|
+
var aDir = a.type === 'dir' ? 1 : 0;
|
|
106
|
+
var bDir = b.type === 'dir' ? 1 : 0;
|
|
107
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
108
|
+
var aRelevance = getMatchRelevance(a, query);
|
|
109
|
+
var bRelevance = getMatchRelevance(b, query);
|
|
110
|
+
if (aRelevance !== bRelevance) return aRelevance - bRelevance;
|
|
111
|
+
if (aRelevance === FUZZY_ONLY && a.score !== b.score) return (b.score || 0) - (a.score || 0);
|
|
112
|
+
var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
|
|
113
|
+
var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
|
|
114
|
+
if (aRecent !== bRecent) return aRecent - bRecent;
|
|
115
|
+
return getFilePriority(a.path) - getFilePriority(b.path);
|
|
116
|
+
});
|
|
117
|
+
return filtered;
|
|
118
|
+
}
|
|
119
|
+
|
|
26
120
|
// ── Component ──────────────────────────────────────────────────────────────
|
|
27
121
|
|
|
28
122
|
var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
@@ -76,49 +170,6 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
76
170
|
if (inputRef.current) inputRef.current.focus();
|
|
77
171
|
};
|
|
78
172
|
|
|
79
|
-
// Priority tier for a file path: lower = shown first.
|
|
80
|
-
// Order: controller > model > helper > concern > view > job > other > noise
|
|
81
|
-
function getFilePriority(path) {
|
|
82
|
-
var p = (path || '').toLowerCase();
|
|
83
|
-
if (p.indexOf('/controllers/') >= 0) return 1;
|
|
84
|
-
if (p.indexOf('/models/') >= 0) return 2;
|
|
85
|
-
if (p.indexOf('/helpers/') >= 0) return 3;
|
|
86
|
-
if (p.indexOf('/concerns/') >= 0) return 4;
|
|
87
|
-
if (p.indexOf('/views/') >= 0) return 5;
|
|
88
|
-
if (p.indexOf('/jobs/') >= 0) return 6;
|
|
89
|
-
// Deprioritise: migrations, schema, compiled assets, vendor, lock files
|
|
90
|
-
if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
|
|
91
|
-
if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
|
|
92
|
-
if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
|
|
93
|
-
p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
|
|
94
|
-
return 50;
|
|
95
|
-
}
|
|
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
|
-
|
|
111
|
-
// Match relevance within a priority tier: exact basename > prefix > substring > other.
|
|
112
|
-
function getMatchRelevance(result, q) {
|
|
113
|
-
if (!q) return 3;
|
|
114
|
-
var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
|
|
115
|
-
var lq = q.toLowerCase();
|
|
116
|
-
if (name === lq) return 0;
|
|
117
|
-
if (name.slice(0, lq.length) === lq) return 1;
|
|
118
|
-
if (name.indexOf(lq) >= 0) return 2;
|
|
119
|
-
return 3;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
173
|
var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
|
|
123
174
|
if (type === 'dir') {
|
|
124
175
|
return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
|
|
@@ -143,37 +194,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
143
194
|
// path in the workspace ran on every keystroke, between the keypress and
|
|
144
195
|
// the character appearing.
|
|
145
196
|
var timer = setTimeout(function () {
|
|
146
|
-
var
|
|
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
|
-
});
|
|
197
|
+
var filtered = rankResults(SearchService.searchFiles(query), query, showFolders);
|
|
177
198
|
setResults(filtered.slice(0, 200));
|
|
178
199
|
setSelectedIndex(0);
|
|
179
200
|
}, 120);
|
|
@@ -395,7 +416,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
395
416
|
}),
|
|
396
417
|
results.length === 0 && React.createElement(
|
|
397
418
|
'div',
|
|
398
|
-
{ style: { padding: '
|
|
419
|
+
{ style: { padding: '14px 18px', color: '#666', fontSize: '14px' } },
|
|
399
420
|
'No matching files.'
|
|
400
421
|
)
|
|
401
422
|
)
|
|
@@ -404,4 +425,5 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
404
425
|
);
|
|
405
426
|
};
|
|
406
427
|
|
|
428
|
+
QuickOpenDialog.rankResults = rankResults;
|
|
407
429
|
window.QuickOpenDialog = QuickOpenDialog;
|
|
@@ -1484,7 +1484,13 @@
|
|
|
1484
1484
|
cases: {
|
|
1485
1485
|
'$2~(?i:SQL)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'sql' },
|
|
1486
1486
|
'$2~(?i:HTML?|ERB)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'html' },
|
|
1487
|
-
|
|
1487
|
+
// JSX rides the javascript tokenizer: Monaco registers no
|
|
1488
|
+
// separate jsx language, and its TS-derived grammar lexes JSX
|
|
1489
|
+
// bodies fine. The guard is anchored (^...$), so plain `JS`
|
|
1490
|
+
// never matched `JSX` and it fell through to the generic
|
|
1491
|
+
// heredoc — the whole block painted as one string.
|
|
1492
|
+
'$2~(?i:JSX?|JAVASCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'javascript' },
|
|
1493
|
+
'$2~(?i:TSX?|TYPESCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'typescript' },
|
|
1488
1494
|
'$2~(?i:CSS)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'css' },
|
|
1489
1495
|
'$2~(?i:SCSS)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'scss' },
|
|
1490
1496
|
'$2~(?i:JSON)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'json' },
|
|
@@ -276,30 +276,21 @@ var FileService = (function () {
|
|
|
276
276
|
|
|
277
277
|
// line (1-based, optional) narrows the run to the single test at that line;
|
|
278
278
|
// the server ignores it unless `path` IS the test file.
|
|
279
|
-
// Server-reported
|
|
280
|
-
// timeout is derived from
|
|
279
|
+
// Server-reported ceiling (seconds), seeded from /workspace. The request
|
|
280
|
+
// timeout is derived from it rather than hard-coded: two independent
|
|
281
281
|
// numbers meant that raising config.test_timeout past the client's own cap
|
|
282
282
|
// made the browser abort a run the server was still executing, and the user
|
|
283
283
|
// saw a generic network error instead of the server's message.
|
|
284
|
-
var
|
|
285
|
-
function
|
|
286
|
-
if (t && t.test > 0) testTimeouts.test = t.test;
|
|
287
|
-
if (t && t.testAll > 0) testTimeouts.testAll = t.testAll;
|
|
288
|
-
}
|
|
289
|
-
// Margin for boot, JSON encoding and the trip back, so the server's own
|
|
290
|
-
// timeout is always the one that fires first and reports why.
|
|
291
|
-
function requestTimeout(seconds) { return (seconds + 30) * 1000; }
|
|
284
|
+
var testTimeout = 180;
|
|
285
|
+
function setTestTimeout(seconds) { if (seconds > 0) testTimeout = seconds; }
|
|
292
286
|
|
|
293
287
|
function runTests(path, line) {
|
|
294
288
|
var payload = { path: path };
|
|
295
289
|
if (line) payload.line = line;
|
|
290
|
+
// Margin for boot, JSON encoding and the trip back, so the server's own
|
|
291
|
+
// timeout is always the one that fires first and reports why.
|
|
296
292
|
return axios.post(window.mbeditorBasePath() + '/test', payload,
|
|
297
|
-
{ timeout:
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function runAllTests() {
|
|
301
|
-
return axios.post(window.mbeditorBasePath() + '/test_all', {},
|
|
302
|
-
{ timeout: requestTimeout(testTimeouts.testAll) }).then(function(res) { return res.data; });
|
|
293
|
+
{ timeout: (testTimeout + 30) * 1000 }).then(function(res) { return res.data; });
|
|
303
294
|
}
|
|
304
295
|
|
|
305
296
|
function ping() {
|
|
@@ -585,8 +576,7 @@ var FileService = (function () {
|
|
|
585
576
|
rubyRename: rubyRename,
|
|
586
577
|
getModelGraph: getModelGraph,
|
|
587
578
|
runRubocop: runRubocop,
|
|
588
|
-
|
|
589
|
-
setTestTimeouts: setTestTimeouts,
|
|
579
|
+
setTestTimeout: setTestTimeout,
|
|
590
580
|
getExceptions: getExceptions,
|
|
591
581
|
clearExceptions: clearExceptions,
|
|
592
582
|
getRelatedFiles: getRelatedFiles,
|
|
@@ -79,13 +79,31 @@ var SearchService = (function () {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Fuzziness per term: edit distance is `fuzzy * term.length` inside
|
|
83
|
+
// MiniSearch, so short terms get none (at 3 chars every 3-letter token in the
|
|
84
|
+
// tree is within one edit and the list becomes noise) and longer ones get
|
|
85
|
+
// roughly one typo per 4 characters.
|
|
86
|
+
function _fuzzyFor(term) {
|
|
87
|
+
return term.length > 3 ? 0.25 : false;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
// Search files (and optionally folders) in the local MiniSearch index.
|
|
91
|
+
// Fuzzy + prefix, so a misspelled or half-typed query still finds the file;
|
|
92
|
+
// ranking back in QuickOpenDialog puts exact matches first, so loosening the
|
|
93
|
+
// match here cannot demote one.
|
|
83
94
|
// Also performs a case-insensitive substring scan so that partial-word
|
|
84
|
-
// queries like "project" reliably find "projects_controller.rb"
|
|
95
|
+
// queries like "project" reliably find "projects_controller.rb" — the
|
|
96
|
+
// tokenizer splits on punctuation, so a query spanning a separator
|
|
97
|
+
// ("models/user") tokenizes into terms no single token can satisfy.
|
|
85
98
|
// Returns merged results; MiniSearch scored entries come first.
|
|
86
99
|
function searchFiles(query) {
|
|
87
100
|
if (!query) return [];
|
|
88
|
-
var msResults = _miniSearch.search(query, {
|
|
101
|
+
var msResults = _miniSearch.search(query, {
|
|
102
|
+
prefix: true,
|
|
103
|
+
fuzzy: _fuzzyFor,
|
|
104
|
+
combineWith: 'AND',
|
|
105
|
+
boost: { name: 2 }
|
|
106
|
+
});
|
|
89
107
|
// Substring fallback — catch anything MiniSearch missed
|
|
90
108
|
var q = query.toLowerCase();
|
|
91
109
|
var msIds = new Set(msResults.map(function(r) { return r.id; }));
|
|
@@ -169,7 +169,10 @@ var TabManager = (function () {
|
|
|
169
169
|
openTab(target.path, target.name, target.line, null, false, target.col);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
// col/endCol are the 1-based bounds of the thing being jumped to. Pass both
|
|
173
|
+
// and the editor selects it, leaving the cursor at its end; pass col alone
|
|
174
|
+
// and it just parks the cursor there.
|
|
175
|
+
function openTab(path, name, line, forcePaneId, isSoftOpen, col, endCol) {
|
|
173
176
|
if (line) _jumpOrigin = _snapshotPosition() || _jumpOrigin;
|
|
174
177
|
var state = EditorStore.getState();
|
|
175
178
|
var paneId = forcePaneId || state.focusedPaneId;
|
|
@@ -191,7 +194,7 @@ var TabManager = (function () {
|
|
|
191
194
|
var existing = pane.tabs.find(function(t) { return t.path === path; });
|
|
192
195
|
|
|
193
196
|
if (existing) {
|
|
194
|
-
if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null });
|
|
197
|
+
if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null, gotoEndCol: endCol || null });
|
|
195
198
|
switchTab(paneId, path);
|
|
196
199
|
if (_isMarkdownPath(path)) {
|
|
197
200
|
_ensureMarkdownPreview(paneId, path, existing.name || name, existing.content || "");
|
|
@@ -217,7 +220,7 @@ var TabManager = (function () {
|
|
|
217
220
|
isSoftOpen: isSoftOpen ? true : false,
|
|
218
221
|
loading: true
|
|
219
222
|
};
|
|
220
|
-
if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; }
|
|
223
|
+
if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; newTab.gotoEndCol = endCol || null; }
|
|
221
224
|
|
|
222
225
|
var newPanes = state.panes.map(function(p) {
|
|
223
226
|
if (p.id === paneId) {
|
|
@@ -699,7 +702,7 @@ var TabManager = (function () {
|
|
|
699
702
|
}
|
|
700
703
|
|
|
701
704
|
function clearGotoLine(paneId, path) {
|
|
702
|
-
_updateTab(paneId, path, { gotoLine: null, gotoCol: null });
|
|
705
|
+
_updateTab(paneId, path, { gotoLine: null, gotoCol: null, gotoEndCol: null });
|
|
703
706
|
}
|
|
704
707
|
|
|
705
708
|
// VS Code-style scratch buffer: a tab with no file behind it. Nothing is
|