mbeditor 0.10.1 → 0.12.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 +191 -0
- data/README.md +226 -3
- data/app/assets/javascripts/mbeditor/application.js +5 -0
- data/app/assets/javascripts/mbeditor/application_iife_tail.js +6 -0
- data/app/assets/javascripts/mbeditor/collaboration_identity.js +234 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +690 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +120 -19
- data/app/assets/javascripts/mbeditor/components/FileTree.js +127 -8
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +12 -3
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +127 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +948 -72
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +565 -0
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +130 -10
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +1 -0
- data/app/assets/javascripts/mbeditor/components/TabBar.js +4 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +661 -140
- data/app/assets/javascripts/mbeditor/file_import.js +146 -0
- data/app/assets/javascripts/mbeditor/file_service.js +68 -3
- data/app/assets/javascripts/mbeditor/tab_manager.js +50 -1
- data/app/assets/javascripts/mbeditor/websocket_service.js +89 -0
- data/app/assets/stylesheets/mbeditor/editor.css +273 -10
- data/app/channels/mbeditor/channel_authentication.rb +94 -0
- data/app/channels/mbeditor/collaboration_channel.rb +84 -0
- data/app/channels/mbeditor/editor_channel.rb +40 -1
- data/app/controllers/mbeditor/application_controller.rb +5 -1
- data/app/controllers/mbeditor/editors_controller.rb +481 -19
- data/app/controllers/mbeditor/git_controller.rb +9 -2
- data/app/services/mbeditor/availability_probe.rb +76 -17
- data/app/services/mbeditor/code_search_service.rb +23 -3
- data/app/services/mbeditor/collaboration_doc_store.rb +116 -0
- data/app/services/mbeditor/file_import_service.rb +103 -0
- data/app/services/mbeditor/git_combined_diff_service.rb +36 -5
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/git_service.rb +22 -6
- data/app/services/mbeditor/js_globals_service.rb +31 -2
- data/app/services/mbeditor/js_program_service.rb +173 -0
- data/app/services/mbeditor/lsp_diagnostics_translator.rb +99 -5
- data/app/services/mbeditor/model_graph_service.rb +232 -0
- data/app/services/mbeditor/presence_registry.rb +83 -0
- data/app/services/mbeditor/ri_definition_service.rb +39 -5
- data/app/services/mbeditor/search_replace_service.rb +24 -4
- data/app/views/layouts/mbeditor/application.html.erb +2 -0
- data/lib/mbeditor/configuration.rb +43 -3
- data/lib/mbeditor/engine.rb +34 -0
- data/lib/mbeditor/exception_log.rb +84 -0
- data/lib/mbeditor/route_map.rb +6 -0
- data/lib/mbeditor/ruby_lsp_client.rb +28 -1
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/vendor/assets/javascripts/yjs-collab.js +12 -0
- metadata +16 -2
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
var ProblemsPanel = (function () {
|
|
11
11
|
var SEVERITY_ERROR = 8;
|
|
12
12
|
var SEVERITY_WARNING = 4;
|
|
13
|
+
var SEVERITY_INFO = 2;
|
|
14
|
+
var SEVERITY_HINT = 1;
|
|
15
|
+
|
|
16
|
+
// Info and Hint share a row style: both mean "worth knowing, not worth
|
|
17
|
+
// stopping for", and two shades of grey would be a distinction without a
|
|
18
|
+
// difference in a list this dense.
|
|
19
|
+
var SEVERITY_KIND = { 8: 'error', 4: 'warning', 2: 'info', 1: 'info' };
|
|
20
|
+
var SEVERITY_ICON = {
|
|
21
|
+
error: 'fa-bug',
|
|
22
|
+
warning: 'fa-exclamation-triangle',
|
|
23
|
+
info: 'fa-info-circle'
|
|
24
|
+
};
|
|
25
|
+
var SEVERITY_LABEL = { error: 'Error', warning: 'Warning', info: 'Info' };
|
|
13
26
|
// Long enough to recognise the statement, short enough not to push the
|
|
14
27
|
// location off the end of the row.
|
|
15
28
|
var CODE_PREVIEW_LIMIT = 120;
|
|
@@ -28,10 +41,14 @@ var ProblemsPanel = (function () {
|
|
|
28
41
|
// counts share this one pass. Exposed on the component for the status bar.
|
|
29
42
|
function collect() {
|
|
30
43
|
var monaco = window.monaco;
|
|
31
|
-
if (!monaco || !monaco.editor) return { errors: [], warnings: [], byFile: [] };
|
|
44
|
+
if (!monaco || !monaco.editor) return { errors: [], warnings: [], infos: [], byFile: [] };
|
|
32
45
|
|
|
33
46
|
var errors = [];
|
|
34
47
|
var warnings = [];
|
|
48
|
+
// RuboCop's convention and refactor offenses grade as Info, and its own
|
|
49
|
+
// `info` level as Hint. Listing only errors and warnings would hide the
|
|
50
|
+
// majority of a lint run.
|
|
51
|
+
var infos = [];
|
|
35
52
|
var byFile = [];
|
|
36
53
|
|
|
37
54
|
monaco.editor.getModels().forEach(function (model) {
|
|
@@ -39,7 +56,8 @@ var ProblemsPanel = (function () {
|
|
|
39
56
|
if (!path || model.isDisposed()) return;
|
|
40
57
|
|
|
41
58
|
var markers = monaco.editor.getModelMarkers({ resource: model.uri }).filter(function (m) {
|
|
42
|
-
return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING
|
|
59
|
+
return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING ||
|
|
60
|
+
m.severity === SEVERITY_INFO || m.severity === SEVERITY_HINT;
|
|
43
61
|
});
|
|
44
62
|
if (markers.length === 0) return;
|
|
45
63
|
|
|
@@ -48,7 +66,10 @@ var ProblemsPanel = (function () {
|
|
|
48
66
|
// Carry the source line alongside the marker: read here, while the model
|
|
49
67
|
// is in hand, rather than looking the model up again at render time.
|
|
50
68
|
var entries = markers.map(function (m) {
|
|
51
|
-
|
|
69
|
+
var bucket = m.severity === SEVERITY_ERROR ? errors
|
|
70
|
+
: m.severity === SEVERITY_WARNING ? warnings
|
|
71
|
+
: infos;
|
|
72
|
+
bucket.push(m);
|
|
52
73
|
return { marker: m, code: codePreview(model, m.startLineNumber) };
|
|
53
74
|
});
|
|
54
75
|
|
|
@@ -56,9 +77,11 @@ var ProblemsPanel = (function () {
|
|
|
56
77
|
});
|
|
57
78
|
|
|
58
79
|
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 };
|
|
80
|
+
return { errors: errors, warnings: warnings, infos: infos, byFile: byFile };
|
|
60
81
|
}
|
|
61
82
|
|
|
83
|
+
// The status bar deliberately tallies only errors and warnings: a count that
|
|
84
|
+
// included every convention offense would be a number nobody acts on.
|
|
62
85
|
function counts() {
|
|
63
86
|
var all = collect();
|
|
64
87
|
return { errors: all.errors.length, warnings: all.warnings.length };
|
|
@@ -73,6 +96,38 @@ var ProblemsPanel = (function () {
|
|
|
73
96
|
var _filter = React.useState('');
|
|
74
97
|
var filter = _filter[0], setFilter = _filter[1];
|
|
75
98
|
|
|
99
|
+
// Exceptions raised by the host app. Unlike markers these are not per-model
|
|
100
|
+
// — a runtime failure isn't a property of a file you happen to have open —
|
|
101
|
+
// so they live in their own section above the marker list.
|
|
102
|
+
var _exceptions = React.useState([]);
|
|
103
|
+
var exceptions = _exceptions[0], setExceptions = _exceptions[1];
|
|
104
|
+
|
|
105
|
+
React.useEffect(function () {
|
|
106
|
+
var cancelled = false;
|
|
107
|
+
if (FileService.getExceptions) {
|
|
108
|
+
FileService.getExceptions().then(function (data) {
|
|
109
|
+
if (!cancelled) setExceptions((data && data.exceptions) || []);
|
|
110
|
+
})["catch"](function () { /* older host, or capture disabled */ });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Live push while the panel is open.
|
|
114
|
+
var onException = function (e) {
|
|
115
|
+
setExceptions(function (prev) {
|
|
116
|
+
return [e.detail].concat(prev).slice(0, 50);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('mbeditor:exception', onException);
|
|
120
|
+
return function () {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
window.removeEventListener('mbeditor:exception', onException);
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
var clearExceptions = function () {
|
|
127
|
+
setExceptions([]);
|
|
128
|
+
if (FileService.clearExceptions) FileService.clearExceptions()["catch"](function () {});
|
|
129
|
+
};
|
|
130
|
+
|
|
76
131
|
var MIN_HEIGHT = 120;
|
|
77
132
|
var _height = React.useState(function () {
|
|
78
133
|
var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
|
|
@@ -122,7 +177,7 @@ var ProblemsPanel = (function () {
|
|
|
122
177
|
}).filter(function (entry) { return entry.markers.length > 0; })
|
|
123
178
|
: problems.byFile;
|
|
124
179
|
|
|
125
|
-
var total = problems.errors.length + problems.warnings.length;
|
|
180
|
+
var total = problems.errors.length + problems.warnings.length + problems.infos.length;
|
|
126
181
|
|
|
127
182
|
return React.createElement(
|
|
128
183
|
'div',
|
|
@@ -142,6 +197,7 @@ var ProblemsPanel = (function () {
|
|
|
142
197
|
{ className: 'ide-problems-summary' },
|
|
143
198
|
problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
|
|
144
199
|
', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
|
|
200
|
+
', ' + problems.infos.length + ' info' +
|
|
145
201
|
' in open files'
|
|
146
202
|
),
|
|
147
203
|
React.createElement('input', {
|
|
@@ -159,7 +215,57 @@ var ProblemsPanel = (function () {
|
|
|
159
215
|
React.createElement(
|
|
160
216
|
'div',
|
|
161
217
|
{ className: 'ide-problems-body' },
|
|
162
|
-
|
|
218
|
+
exceptions.length > 0 && React.createElement(
|
|
219
|
+
'div',
|
|
220
|
+
{ className: 'ide-problems-file ide-problems-exceptions' },
|
|
221
|
+
React.createElement(
|
|
222
|
+
'div',
|
|
223
|
+
{ className: 'ide-problems-file-name' },
|
|
224
|
+
React.createElement('i', { className: 'fas fa-bolt', 'aria-hidden': 'true' }),
|
|
225
|
+
' Exceptions',
|
|
226
|
+
React.createElement('span', { className: 'ide-problems-file-count' }, exceptions.length),
|
|
227
|
+
React.createElement('button', {
|
|
228
|
+
type: 'button', className: 'ide-problems-btn ide-problems-clear',
|
|
229
|
+
title: 'Clear recorded exceptions', onClick: clearExceptions
|
|
230
|
+
}, React.createElement('i', { className: 'fas fa-times' }))
|
|
231
|
+
),
|
|
232
|
+
exceptions.map(function (ex) {
|
|
233
|
+
return React.createElement(
|
|
234
|
+
'div',
|
|
235
|
+
{ className: 'ide-problems-exception', key: ex.id },
|
|
236
|
+
React.createElement(
|
|
237
|
+
'div',
|
|
238
|
+
{ className: 'ide-problems-item ide-problems-item-error ide-problems-exception-head' },
|
|
239
|
+
React.createElement('i', { className: 'fas fa-bolt ide-problems-icon', 'aria-hidden': 'true' }),
|
|
240
|
+
React.createElement('span', { className: 'ide-problems-msg' }, ex.klass + ': ' + ex.message),
|
|
241
|
+
(ex.controller || ex.path) && React.createElement(
|
|
242
|
+
'span',
|
|
243
|
+
{ className: 'ide-problems-source' },
|
|
244
|
+
ex.controller ? ex.controller + '#' + ex.action : ex.path
|
|
245
|
+
)
|
|
246
|
+
),
|
|
247
|
+
// Only workspace frames survive the server side, so every one of
|
|
248
|
+
// these is a file this editor can actually open.
|
|
249
|
+
(ex.frames || []).map(function (f, i) {
|
|
250
|
+
return React.createElement(
|
|
251
|
+
'button',
|
|
252
|
+
{
|
|
253
|
+
type: 'button',
|
|
254
|
+
className: 'ide-problems-item ide-problems-frame',
|
|
255
|
+
key: ex.id + ':' + i,
|
|
256
|
+
'aria-label': 'Open ' + f.file + ' line ' + f.line,
|
|
257
|
+
onClick: function () { if (onOpenFile) onOpenFile(f.file, f.line, 1); }
|
|
258
|
+
},
|
|
259
|
+
React.createElement('span', { className: 'ide-problems-code' }, f.file),
|
|
260
|
+
React.createElement('span', { className: 'ide-problems-loc' }, '[' + f.line + ']')
|
|
261
|
+
);
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
})
|
|
265
|
+
),
|
|
266
|
+
total === 0 && exceptions.length > 0
|
|
267
|
+
? null
|
|
268
|
+
: total === 0
|
|
163
269
|
? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
|
|
164
270
|
: shown.length === 0
|
|
165
271
|
? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems match the filter')
|
|
@@ -175,14 +281,17 @@ var ProblemsPanel = (function () {
|
|
|
175
281
|
),
|
|
176
282
|
entry.markers.map(function (item, index) {
|
|
177
283
|
var marker = item.marker;
|
|
178
|
-
var
|
|
284
|
+
var kind = SEVERITY_KIND[marker.severity] || 'info';
|
|
285
|
+
var docsHref = marker.code && marker.code.target
|
|
286
|
+
? String(marker.code.target)
|
|
287
|
+
: null;
|
|
179
288
|
return React.createElement(
|
|
180
289
|
'button',
|
|
181
290
|
{
|
|
182
291
|
type: 'button',
|
|
183
|
-
className: 'ide-problems-item ide-problems-item-' +
|
|
292
|
+
className: 'ide-problems-item ide-problems-item-' + kind,
|
|
184
293
|
key: entry.path + ':' + marker.startLineNumber + ':' + index,
|
|
185
|
-
'aria-label':
|
|
294
|
+
'aria-label': SEVERITY_LABEL[kind] + ': ' + marker.message +
|
|
186
295
|
', ' + entry.path + ' line ' + marker.startLineNumber +
|
|
187
296
|
(item.code ? ', source: ' + item.code : ''),
|
|
188
297
|
onClick: function () {
|
|
@@ -190,11 +299,22 @@ var ProblemsPanel = (function () {
|
|
|
190
299
|
}
|
|
191
300
|
},
|
|
192
301
|
React.createElement('i', {
|
|
193
|
-
className: 'fas ' +
|
|
302
|
+
className: 'fas ' + SEVERITY_ICON[kind] + ' ide-problems-icon',
|
|
194
303
|
'aria-hidden': 'true'
|
|
195
304
|
}),
|
|
196
305
|
React.createElement('span', { className: 'ide-problems-msg' }, marker.message),
|
|
197
306
|
item.code && React.createElement('code', { className: 'ide-problems-code' }, item.code),
|
|
307
|
+
// RuboCop ships a docs URL per cop; when the marker carries
|
|
308
|
+
// one, the cop name becomes a link out to it. stopPropagation
|
|
309
|
+
// so following it doesn't also jump the editor to the offense.
|
|
310
|
+
docsHref && React.createElement('a', {
|
|
311
|
+
className: 'ide-problems-docs',
|
|
312
|
+
href: docsHref,
|
|
313
|
+
target: '_blank',
|
|
314
|
+
rel: 'noopener noreferrer',
|
|
315
|
+
title: 'Documentation for ' + marker.code.value,
|
|
316
|
+
onClick: function (e) { e.stopPropagation(); }
|
|
317
|
+
}, marker.code.value),
|
|
198
318
|
marker.source && React.createElement('span', { className: 'ide-problems-source' }, marker.source),
|
|
199
319
|
React.createElement(
|
|
200
320
|
'span',
|
|
@@ -102,6 +102,7 @@ var ShortcutHelp = function ShortcutHelp(_ref) {
|
|
|
102
102
|
'tbody',
|
|
103
103
|
null,
|
|
104
104
|
React.createElement(Row, { keys: 'Ctrl+P', desc: 'Quick-open any file by name' }),
|
|
105
|
+
React.createElement(Row, { keys: 'PgUp/PgDn', desc: 'Cycle between cursor position and last jump origin' }),
|
|
105
106
|
React.createElement(Row, { keys: 'Ctrl+S', desc: 'Save the active file' }),
|
|
106
107
|
React.createElement(Row, { keys: 'Ctrl+Shift+G', desc: 'Toggle git panel' }),
|
|
107
108
|
React.createElement(Row, { keys: 'Ctrl+Shift+L', desc: 'Toggle Rails log panel' }),
|
|
@@ -60,9 +60,11 @@ var TabBar = function TabBar(_ref) {
|
|
|
60
60
|
});
|
|
61
61
|
if (hasError) return 'tab-has-error';
|
|
62
62
|
|
|
63
|
+
// Only real warnings tint the tab. Convention and refactor offenses grade
|
|
64
|
+
// as info/hint and are far too common to be worth an amber tab — a tint
|
|
65
|
+
// that's always on tells you nothing.
|
|
63
66
|
var hasWarning = tabMarkers.some(function (marker) {
|
|
64
|
-
|
|
65
|
-
return severity !== 'error' && severity !== 'fatal';
|
|
67
|
+
return String(marker && marker.severity || '').toLowerCase() === 'warning';
|
|
66
68
|
});
|
|
67
69
|
if (hasWarning) return 'tab-has-warning';
|
|
68
70
|
|