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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TestRunPanel — bottom drawer for a whole-suite run.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately not a modal, unlike the per-file TestResultsPanel: a suite run
|
|
6
|
+
// takes minutes, and a backdrop that blocks the editor for the duration turns
|
|
7
|
+
// the wait into dead time. This docks alongside Problems and the log, so you
|
|
8
|
+
// can keep reading code while it runs and keep the failures on screen while
|
|
9
|
+
// you fix them.
|
|
10
|
+
//
|
|
11
|
+
// Structure and CSS are shared with ProblemsPanel on purpose — same drawer,
|
|
12
|
+
// same header, same clickable rows — rather than a second near-identical
|
|
13
|
+
// stylesheet.
|
|
14
|
+
var TestRunPanel = (function () {
|
|
15
|
+
var STATUS_ICON = {
|
|
16
|
+
pass: 'fa-check-circle',
|
|
17
|
+
fail: 'fa-times-circle',
|
|
18
|
+
error: 'fa-exclamation-circle',
|
|
19
|
+
skip: 'fa-forward'
|
|
20
|
+
};
|
|
21
|
+
var STATUS_KIND = { fail: 'error', error: 'error', skip: 'info', pass: 'info' };
|
|
22
|
+
|
|
23
|
+
function isFailure(t) { return t.status === 'fail' || t.status === 'error'; }
|
|
24
|
+
|
|
25
|
+
// Closing the drawer unmounts it, so the result is kept outside the
|
|
26
|
+
// component. localStorage rather than a module variable, to match the
|
|
27
|
+
// per-file test cache in MbeditorApp — a suite run is the most expensive
|
|
28
|
+
// thing the editor can ask for, and losing it to a page reload is the same
|
|
29
|
+
// annoyance as losing it to a close.
|
|
30
|
+
var CACHE_KEY = 'mbeditor_test_result_suite';
|
|
31
|
+
// The runner's raw output is unbounded — a verbose suite easily runs to
|
|
32
|
+
// megabytes, and a quota error means nothing is cached at all, which is the
|
|
33
|
+
// exact failure this is here to prevent. The tail is the part worth keeping:
|
|
34
|
+
// failures and the summary line are at the end.
|
|
35
|
+
var RAW_CACHE_LIMIT = 200000;
|
|
36
|
+
|
|
37
|
+
function loadCached() {
|
|
38
|
+
try {
|
|
39
|
+
var stored = window.localStorage.getItem(CACHE_KEY);
|
|
40
|
+
return stored ? JSON.parse(stored) : null;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveCached(result) {
|
|
47
|
+
try {
|
|
48
|
+
var raw = result && result.raw;
|
|
49
|
+
var slim = (raw && raw.length > RAW_CACHE_LIMIT)
|
|
50
|
+
? Object.assign({}, result, { raw: '…output truncated…\n' + raw.slice(-RAW_CACHE_LIMIT) })
|
|
51
|
+
: result;
|
|
52
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify(slim));
|
|
53
|
+
} catch (e) { /* quota or storage blocked — the run still shows, just isn't kept */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearCached() {
|
|
57
|
+
try { window.localStorage.removeItem(CACHE_KEY); } catch (e) {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ranAtLabel(result) {
|
|
61
|
+
if (!result || !result.cachedAt) return null;
|
|
62
|
+
try {
|
|
63
|
+
return 'ran ' + new Date(result.cachedAt).toLocaleTimeString();
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Failures grouped by the file they live in — the unit you actually open.
|
|
70
|
+
// Anything the server could not resolve to a workspace-relative path is
|
|
71
|
+
// still listed, just not openable.
|
|
72
|
+
function byFile(tests) {
|
|
73
|
+
var order = [];
|
|
74
|
+
var groups = {};
|
|
75
|
+
tests.filter(isFailure).forEach(function (t) {
|
|
76
|
+
var key = t.file || '';
|
|
77
|
+
if (!groups[key]) { groups[key] = []; order.push(key); }
|
|
78
|
+
groups[key].push(t);
|
|
79
|
+
});
|
|
80
|
+
return order.map(function (k) { return { path: k, tests: groups[k] }; });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var Panel = function TestRunPanelComponent(_ref) {
|
|
84
|
+
var onClose = _ref.onClose;
|
|
85
|
+
var onOpenFile = _ref.onOpenFile;
|
|
86
|
+
// Hands the result to the app so the editors can draw the same inline
|
|
87
|
+
// pass/fail zones a per-file run draws. Without it, opening a failing file
|
|
88
|
+
// from this drawer showed a bare editor.
|
|
89
|
+
var onResult = _ref.onResult;
|
|
90
|
+
|
|
91
|
+
var _result = React.useState(loadCached);
|
|
92
|
+
var result = _result[0], setResult = _result[1];
|
|
93
|
+
|
|
94
|
+
// Kept in a ref so the publish effect below can fire on mount without
|
|
95
|
+
// listing onResult (a fresh closure every render) as a dependency.
|
|
96
|
+
var onResultRef = React.useRef(onResult);
|
|
97
|
+
onResultRef.current = onResult;
|
|
98
|
+
|
|
99
|
+
// Publish whatever the drawer is showing, including a result restored from
|
|
100
|
+
// the cache on mount — reopening the drawer should put the decorations
|
|
101
|
+
// back, not just the list.
|
|
102
|
+
React.useEffect(function () {
|
|
103
|
+
if (onResultRef.current) onResultRef.current(result);
|
|
104
|
+
}, [result]);
|
|
105
|
+
var _running = React.useState(false);
|
|
106
|
+
var running = _running[0], setRunning = _running[1];
|
|
107
|
+
var _showRaw = React.useState(false);
|
|
108
|
+
var showRaw = _showRaw[0], setShowRaw = _showRaw[1];
|
|
109
|
+
|
|
110
|
+
// Elapsed seconds while a run is in flight. A suite run is long enough
|
|
111
|
+
// that a spinner with no number reads as "hung".
|
|
112
|
+
var _elapsed = React.useState(0);
|
|
113
|
+
var elapsed = _elapsed[0], setElapsed = _elapsed[1];
|
|
114
|
+
React.useEffect(function () {
|
|
115
|
+
if (!running) return;
|
|
116
|
+
var started = Date.now();
|
|
117
|
+
var id = setInterval(function () {
|
|
118
|
+
setElapsed(Math.round((Date.now() - started) / 1000));
|
|
119
|
+
}, 1000);
|
|
120
|
+
return function () { clearInterval(id); };
|
|
121
|
+
}, [running]);
|
|
122
|
+
|
|
123
|
+
var run = function () {
|
|
124
|
+
if (running) return;
|
|
125
|
+
setRunning(true);
|
|
126
|
+
setElapsed(0);
|
|
127
|
+
var finish = function (data) {
|
|
128
|
+
var stamped = Object.assign({}, data, { cachedAt: Date.now() });
|
|
129
|
+
setResult(stamped);
|
|
130
|
+
saveCached(stamped);
|
|
131
|
+
};
|
|
132
|
+
FileService.runAllTests()
|
|
133
|
+
.then(finish)
|
|
134
|
+
["catch"](function (e) {
|
|
135
|
+
var res = e && e.response && e.response.data;
|
|
136
|
+
finish(res || { ok: false, error: (e && e.message) || 'Test run failed', tests: [] });
|
|
137
|
+
})
|
|
138
|
+
.then(function () { setRunning(false); });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
var failures = byFile((result && result.tests) || []);
|
|
142
|
+
var openable = failures.filter(function (g) { return g.path; });
|
|
143
|
+
|
|
144
|
+
var openFailing = function () {
|
|
145
|
+
if (!onOpenFile) return;
|
|
146
|
+
openable.forEach(function (g) {
|
|
147
|
+
onOpenFile(g.path, (g.tests[0] && g.tests[0].line) || 1, 1);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// A checkout changes what the suite would even run, so the old result is
|
|
152
|
+
// dropped rather than left looking current — same rule as the RuboCop
|
|
153
|
+
// snapshot in the Problems panel.
|
|
154
|
+
React.useEffect(function () {
|
|
155
|
+
var onBranchChanged = function () { setResult(null); clearCached(); };
|
|
156
|
+
window.addEventListener('mbeditor:branch-changed', onBranchChanged);
|
|
157
|
+
return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
var MIN_HEIGHT = 120;
|
|
161
|
+
var _height = React.useState(function () {
|
|
162
|
+
var saved = parseInt(window.localStorage.getItem('mbeditorTestRunHeight'), 10);
|
|
163
|
+
return (saved && saved >= MIN_HEIGHT) ? saved : 260;
|
|
164
|
+
});
|
|
165
|
+
var height = _height[0], setHeight = _height[1];
|
|
166
|
+
var heightRef = React.useRef(height);
|
|
167
|
+
heightRef.current = height;
|
|
168
|
+
|
|
169
|
+
var onResizeMouseDown = function (e) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
var startY = e.clientY;
|
|
172
|
+
var startHeight = heightRef.current;
|
|
173
|
+
var onMove = function (ev) {
|
|
174
|
+
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
175
|
+
var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
|
|
176
|
+
setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
|
|
177
|
+
};
|
|
178
|
+
var onUp = function () {
|
|
179
|
+
document.removeEventListener('mousemove', onMove);
|
|
180
|
+
document.removeEventListener('mouseup', onUp);
|
|
181
|
+
window.localStorage.setItem('mbeditorTestRunHeight', String(heightRef.current));
|
|
182
|
+
};
|
|
183
|
+
document.addEventListener('mousemove', onMove);
|
|
184
|
+
document.addEventListener('mouseup', onUp);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
var summary = result && result.summary;
|
|
188
|
+
var failCount = failures.reduce(function (n, g) { return n + g.tests.length; }, 0);
|
|
189
|
+
|
|
190
|
+
return React.createElement(
|
|
191
|
+
'div',
|
|
192
|
+
{ className: 'ide-problems-drawer ide-testrun-drawer', style: { height: height + 'px' } },
|
|
193
|
+
React.createElement('div', {
|
|
194
|
+
className: 'ide-problems-resize', title: 'Drag to resize', onMouseDown: onResizeMouseDown
|
|
195
|
+
}),
|
|
196
|
+
React.createElement(
|
|
197
|
+
'div',
|
|
198
|
+
{ className: 'ide-problems-header' },
|
|
199
|
+
React.createElement('i', { className: 'fas fa-flask' }),
|
|
200
|
+
React.createElement('span', { className: 'ide-problems-title' }, 'Test Run'),
|
|
201
|
+
summary && React.createElement(
|
|
202
|
+
'div',
|
|
203
|
+
{ className: 'ide-problems-severity-filter' },
|
|
204
|
+
React.createElement('span', { className: 'ide-testrun-stat ide-testrun-pass' },
|
|
205
|
+
(summary.passed || 0) + ' passed'),
|
|
206
|
+
React.createElement('span', { className: 'ide-testrun-stat ide-testrun-fail' },
|
|
207
|
+
((summary.failed || 0) + (summary.errored || 0)) + ' failed'),
|
|
208
|
+
(summary.skipped || 0) > 0 && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
209
|
+
summary.skipped + ' skipped'),
|
|
210
|
+
summary.duration != null && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
211
|
+
summary.duration + 's'),
|
|
212
|
+
ranAtLabel(result) && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
213
|
+
ranAtLabel(result))
|
|
214
|
+
),
|
|
215
|
+
React.createElement(
|
|
216
|
+
'div',
|
|
217
|
+
{ className: 'ide-problems-actions' },
|
|
218
|
+
React.createElement('button', {
|
|
219
|
+
type: 'button', className: 'ide-problems-btn',
|
|
220
|
+
disabled: running,
|
|
221
|
+
title: 'Run the whole test suite',
|
|
222
|
+
onClick: run
|
|
223
|
+
},
|
|
224
|
+
React.createElement('i', { className: running ? 'fas fa-spinner fa-spin' : 'fas fa-play' }),
|
|
225
|
+
React.createElement('span', null, running ? ' Running ' + elapsed + 's' : ' Run all')),
|
|
226
|
+
React.createElement('button', {
|
|
227
|
+
type: 'button', className: 'ide-problems-btn',
|
|
228
|
+
disabled: openable.length === 0,
|
|
229
|
+
title: openable.length
|
|
230
|
+
? 'Open all ' + openable.length + ' file(s) with failures'
|
|
231
|
+
: 'No failing files to open',
|
|
232
|
+
onClick: openFailing
|
|
233
|
+
},
|
|
234
|
+
React.createElement('i', { className: 'fas fa-folder-open' }),
|
|
235
|
+
React.createElement('span', null, ' Open failing' + (openable.length ? ' (' + openable.length + ')' : ''))),
|
|
236
|
+
result && result.raw && React.createElement('button', {
|
|
237
|
+
type: 'button',
|
|
238
|
+
className: 'ide-problems-btn' + (showRaw ? ' is-on' : ''),
|
|
239
|
+
title: 'Show the runner’s raw output',
|
|
240
|
+
'aria-pressed': showRaw ? 'true' : 'false',
|
|
241
|
+
onClick: function () { setShowRaw(function (p) { return !p; }); }
|
|
242
|
+
},
|
|
243
|
+
React.createElement('i', { className: 'fas fa-terminal' }),
|
|
244
|
+
React.createElement('span', null, ' Output'))
|
|
245
|
+
),
|
|
246
|
+
React.createElement('button', {
|
|
247
|
+
type: 'button', className: 'ide-problems-btn',
|
|
248
|
+
title: 'Close', onClick: onClose
|
|
249
|
+
}, React.createElement('i', { className: 'fas fa-times' }))
|
|
250
|
+
),
|
|
251
|
+
React.createElement(
|
|
252
|
+
'div',
|
|
253
|
+
{ className: 'ide-problems-body' },
|
|
254
|
+
result && result.error && React.createElement('div',
|
|
255
|
+
{ className: 'ide-problems-empty' }, 'Test run failed: ' + result.error),
|
|
256
|
+
showRaw && result && result.raw
|
|
257
|
+
? React.createElement('pre', { className: 'ide-testrun-raw' }, result.raw)
|
|
258
|
+
: !result && !running
|
|
259
|
+
? React.createElement('div', { className: 'ide-problems-empty' },
|
|
260
|
+
'Press Run all to run the whole suite')
|
|
261
|
+
: running && !result
|
|
262
|
+
? React.createElement('div', { className: 'ide-problems-empty' },
|
|
263
|
+
'Running the suite… ' + elapsed + 's')
|
|
264
|
+
: failCount === 0 && result && !result.error
|
|
265
|
+
? React.createElement('div', { className: 'ide-problems-empty' }, 'No failing tests')
|
|
266
|
+
: failures.map(function (group) {
|
|
267
|
+
return React.createElement(
|
|
268
|
+
'div',
|
|
269
|
+
{ className: 'ide-problems-file', key: 'tr:' + (group.path || '(unknown)') },
|
|
270
|
+
React.createElement(
|
|
271
|
+
'div',
|
|
272
|
+
{ className: 'ide-problems-file-name' },
|
|
273
|
+
React.createElement('i', { className: 'fas fa-flask', 'aria-hidden': 'true' }),
|
|
274
|
+
' ' + (group.path || 'Unknown file'),
|
|
275
|
+
React.createElement('span', { className: 'ide-problems-file-count' }, group.tests.length)
|
|
276
|
+
),
|
|
277
|
+
group.tests.map(function (t, i) {
|
|
278
|
+
var kind = STATUS_KIND[t.status] || 'info';
|
|
279
|
+
return React.createElement(
|
|
280
|
+
'button',
|
|
281
|
+
{
|
|
282
|
+
type: 'button',
|
|
283
|
+
className: 'ide-problems-item ide-problems-item-' + kind,
|
|
284
|
+
key: 'tr:' + (group.path || '') + ':' + i,
|
|
285
|
+
disabled: !group.path,
|
|
286
|
+
title: t.name + (t.message ? '\n' + t.message : ''),
|
|
287
|
+
'aria-label': t.status + ': ' + t.name +
|
|
288
|
+
(group.path ? ', ' + group.path + ' line ' + (t.line || 1) : ''),
|
|
289
|
+
onClick: function () {
|
|
290
|
+
if (group.path && onOpenFile) onOpenFile(group.path, t.line || 1, 1);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
React.createElement('i', {
|
|
294
|
+
className: 'fas ' + (STATUS_ICON[t.status] || 'fa-circle') + ' ide-problems-icon',
|
|
295
|
+
'aria-hidden': 'true'
|
|
296
|
+
}),
|
|
297
|
+
React.createElement('span', { className: 'ide-problems-msg' }, t.name),
|
|
298
|
+
t.message && React.createElement('code', { className: 'ide-problems-code' },
|
|
299
|
+
t.message.split('\n')[0]),
|
|
300
|
+
t.line && React.createElement('span', { className: 'ide-problems-loc' }, '[' + t.line + ']')
|
|
301
|
+
);
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return Panel;
|
|
310
|
+
})();
|
|
311
|
+
|
|
312
|
+
window.TestRunPanel = TestRunPanel;
|