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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -0
  3. data/app/assets/javascripts/mbeditor/application.js +7 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
  5. data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
  6. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +102 -25
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
  9. data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
  10. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
  11. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +598 -130
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +162 -6
  15. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
  17. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
  18. data/app/assets/javascripts/mbeditor/editor_plugins.js +427 -111
  19. data/app/assets/javascripts/mbeditor/file_import.js +78 -0
  20. data/app/assets/javascripts/mbeditor/file_service.js +122 -18
  21. data/app/assets/javascripts/mbeditor/git_service.js +130 -8
  22. data/app/assets/javascripts/mbeditor/history_service.js +33 -38
  23. data/app/assets/javascripts/mbeditor/log_service.js +4 -0
  24. data/app/assets/javascripts/mbeditor/search_service.js +30 -2
  25. data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
  26. data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
  27. data/app/assets/stylesheets/mbeditor/editor.css +268 -53
  28. data/app/channels/mbeditor/channel_authentication.rb +7 -0
  29. data/app/channels/mbeditor/editor_channel.rb +6 -3
  30. data/app/controllers/mbeditor/application_controller.rb +5 -0
  31. data/app/controllers/mbeditor/editors_controller.rb +173 -29
  32. data/app/controllers/mbeditor/git_controller.rb +3 -3
  33. data/app/controllers/mbeditor/logs_controller.rb +3 -1
  34. data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
  35. data/app/services/mbeditor/editor_state_service.rb +36 -26
  36. data/app/services/mbeditor/exclusion_matcher.rb +12 -10
  37. data/app/services/mbeditor/file_operation_service.rb +38 -3
  38. data/app/services/mbeditor/file_tree_service.rb +30 -2
  39. data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
  40. data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
  41. data/app/services/mbeditor/git_diff_service.rb +5 -1
  42. data/app/services/mbeditor/git_info_service.rb +20 -8
  43. data/app/services/mbeditor/git_line_diff_service.rb +6 -2
  44. data/app/services/mbeditor/git_service.rb +65 -15
  45. data/app/services/mbeditor/js_definition_service.rb +3 -1
  46. data/app/services/mbeditor/js_globals_service.rb +7 -4
  47. data/app/services/mbeditor/js_members_service.rb +3 -2
  48. data/app/services/mbeditor/js_program_service.rb +15 -5
  49. data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
  50. data/app/services/mbeditor/process_runner.rb +42 -12
  51. data/app/services/mbeditor/ri_definition_service.rb +8 -1
  52. data/app/services/mbeditor/route_service.rb +45 -8
  53. data/app/services/mbeditor/rubocop_run_service.rb +86 -0
  54. data/app/services/mbeditor/ruby_definition_service.rb +23 -4
  55. data/app/services/mbeditor/schema_service.rb +65 -64
  56. data/app/services/mbeditor/search_replace_service.rb +31 -5
  57. data/app/services/mbeditor/test_runner_service.rb +98 -10
  58. data/lib/mbeditor/cable_log_filter.rb +8 -2
  59. data/lib/mbeditor/configuration.rb +12 -1
  60. data/lib/mbeditor/editor_bootstrap.rb +18 -12
  61. data/lib/mbeditor/engine.rb +22 -3
  62. data/lib/mbeditor/exception_log.rb +2 -2
  63. data/lib/mbeditor/pending_migrations.rb +33 -0
  64. data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
  65. data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
  66. data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
  67. data/lib/mbeditor/route_map.rb +2 -0
  68. data/lib/mbeditor/ruby_lsp_client.rb +71 -12
  69. data/lib/mbeditor/version.rb +1 -1
  70. 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;