mbeditor 0.12.8 → 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 +207 -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 +135 -40
- 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 +629 -137
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
- 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 +444 -116
- 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 +302 -55
- 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 +75 -10
- 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
|
@@ -418,6 +418,8 @@ var TabManager = (function () {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
function closeTab(paneId, path) {
|
|
421
|
+
// A throttled content write must not outlive the tab it belongs to.
|
|
422
|
+
flushContent();
|
|
421
423
|
if (typeof HistoryService !== 'undefined') {
|
|
422
424
|
HistoryService.flushForPath(path);
|
|
423
425
|
}
|
|
@@ -539,23 +541,73 @@ var TabManager = (function () {
|
|
|
539
541
|
EditorStore.setState({ focusedPaneId: paneId, activeTabId: newActive });
|
|
540
542
|
}
|
|
541
543
|
|
|
544
|
+
// Typing calls markDirty/markClean per keystroke, and every store write
|
|
545
|
+
// re-renders the whole app (MbeditorApp subscribes to the entire store). Two
|
|
546
|
+
// rules keep that off the keystroke path:
|
|
547
|
+
//
|
|
548
|
+
// * the dirty flag is only written on a clean<->dirty transition;
|
|
549
|
+
// * `content` is written on a 250 ms trailing edge, so the store sees at
|
|
550
|
+
// most four content updates a second.
|
|
551
|
+
//
|
|
552
|
+
// FLUSH CONTRACT: anything that reads `tab.content` immediately after an edit
|
|
553
|
+
// — save, save-all, format, format-all — must call TabManager.flushContent()
|
|
554
|
+
// first and then re-read the tab from the store, or it works on text up to
|
|
555
|
+
// 250 ms old. Everything else (lint, EOL indicator, markdown preview, the
|
|
556
|
+
// external-change poll) tolerates the lag by design.
|
|
557
|
+
var CONTENT_WRITE_MS = 250;
|
|
558
|
+
var _pendingContent = {}; // "<paneId> <tabId>" -> { paneId, path, content, markdown }
|
|
559
|
+
var _contentTimer = null;
|
|
560
|
+
|
|
561
|
+
function _queueContent(paneId, path, content, updates, markdown) {
|
|
562
|
+
var key = paneId + '' + path;
|
|
563
|
+
if (updates) {
|
|
564
|
+
// A state transition has to land now, and it takes the content with it.
|
|
565
|
+
delete _pendingContent[key];
|
|
566
|
+
updates.content = content;
|
|
567
|
+
_updateTab(paneId, path, updates);
|
|
568
|
+
if (markdown) _syncMarkdownPreviewContent(path, content);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
_pendingContent[key] = { paneId: paneId, path: path, content: content, markdown: markdown };
|
|
572
|
+
if (_contentTimer === null) {
|
|
573
|
+
_contentTimer = setTimeout(flushContent, CONTENT_WRITE_MS);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function flushContent() {
|
|
578
|
+
if (_contentTimer !== null) {
|
|
579
|
+
clearTimeout(_contentTimer);
|
|
580
|
+
_contentTimer = null;
|
|
581
|
+
}
|
|
582
|
+
var pending = _pendingContent;
|
|
583
|
+
_pendingContent = {};
|
|
584
|
+
Object.keys(pending).forEach(function (k) {
|
|
585
|
+
var p = pending[k];
|
|
586
|
+
_updateTab(p.paneId, p.path, { content: p.content });
|
|
587
|
+
if (p.markdown) _syncMarkdownPreviewContent(p.path, p.content);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
542
591
|
function markDirty(paneId, path, content) {
|
|
543
592
|
// Auto-harden any soft-open tab on first edit.
|
|
544
593
|
var st = EditorStore.getState();
|
|
545
594
|
var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
|
|
546
595
|
var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
|
|
547
|
-
var updates =
|
|
596
|
+
var updates = null;
|
|
597
|
+
if (!existingTab || !existingTab.dirty) updates = { dirty: true };
|
|
548
598
|
if (existingTab && existingTab.isSoftOpen) {
|
|
599
|
+
updates = updates || {};
|
|
549
600
|
updates.isSoftOpen = false;
|
|
550
601
|
}
|
|
551
|
-
|
|
552
|
-
if (_isMarkdownPath(path)) {
|
|
553
|
-
_syncMarkdownPreviewContent(path, content);
|
|
554
|
-
}
|
|
602
|
+
_queueContent(paneId, path, content, updates, _isMarkdownPath(path));
|
|
555
603
|
}
|
|
556
604
|
|
|
557
605
|
function markClean(paneId, path, content) {
|
|
558
|
-
|
|
606
|
+
var st = EditorStore.getState();
|
|
607
|
+
var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
|
|
608
|
+
var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
|
|
609
|
+
var updates = (existingTab && !existingTab.dirty) ? null : { dirty: false };
|
|
610
|
+
_queueContent(paneId, path, content, updates, _isMarkdownPath(path));
|
|
559
611
|
}
|
|
560
612
|
|
|
561
613
|
function hardenTab(paneId, path) {
|
|
@@ -694,6 +746,7 @@ var TabManager = (function () {
|
|
|
694
746
|
focusPane: focusPane,
|
|
695
747
|
markDirty: markDirty,
|
|
696
748
|
markClean: markClean,
|
|
749
|
+
flushContent: flushContent,
|
|
697
750
|
hardenTab: hardenTab,
|
|
698
751
|
saveTabViewState: saveTabViewState,
|
|
699
752
|
reorderTabInPane: reorderTabInPane,
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
// otherwise prevent reconnection.
|
|
9
9
|
var WebSocketService = (function () {
|
|
10
10
|
var _consumer = null;
|
|
11
|
+
// True only when _consumer is one we created ourselves; a consumer borrowed
|
|
12
|
+
// from the host app (window.App.cable) is not ours to disconnect.
|
|
13
|
+
var _ownsConsumer = false;
|
|
11
14
|
var _subscription = null;
|
|
12
15
|
var _connected = false;
|
|
16
|
+
// Distinguishes the first successful handshake from a reconnect, which is the
|
|
17
|
+
// point at which collaboration rooms have to be re-established.
|
|
18
|
+
var _everConnected = false;
|
|
13
19
|
// Why collaboration is or is not working, so the editor can say so instead of
|
|
14
20
|
// failing silently. A rejected subscription used to just schedule a reconnect
|
|
15
21
|
// and tell nobody, which made a blocked cable indistinguishable from an empty
|
|
@@ -62,8 +68,10 @@ var WebSocketService = (function () {
|
|
|
62
68
|
// Reuse an existing consumer the host app may have already created (App.cable
|
|
63
69
|
// is the Rails default). Fall back to creating our own.
|
|
64
70
|
if (typeof window.App !== 'undefined' && window.App.cable) {
|
|
71
|
+
_ownsConsumer = false;
|
|
65
72
|
return window.App.cable;
|
|
66
73
|
}
|
|
74
|
+
_ownsConsumer = true;
|
|
67
75
|
return window.ActionCable.createConsumer(_cableUrl());
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -72,10 +80,14 @@ var WebSocketService = (function () {
|
|
|
72
80
|
try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
|
|
73
81
|
_subscription = null;
|
|
74
82
|
}
|
|
75
|
-
|
|
83
|
+
// Only ever disconnect a consumer we created. When the host app already had
|
|
84
|
+
// one (window.App.cable) it is shared: disconnecting it on any mbeditor
|
|
85
|
+
// cable blip took down the host's own channels with it.
|
|
86
|
+
if (_ownsConsumer && _consumer && typeof _consumer.disconnect === 'function') {
|
|
76
87
|
try { _consumer.disconnect(); } catch (e) { /* ignore */ }
|
|
77
88
|
}
|
|
78
89
|
_consumer = null;
|
|
90
|
+
_ownsConsumer = false;
|
|
79
91
|
_connected = false;
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -139,11 +151,21 @@ var WebSocketService = (function () {
|
|
|
139
151
|
{ channel: 'Mbeditor::EditorChannel' },
|
|
140
152
|
{
|
|
141
153
|
connected: function () {
|
|
154
|
+
var reconnected = _everConnected;
|
|
155
|
+
_everConnected = true;
|
|
142
156
|
_connected = true;
|
|
143
157
|
_status = 'connected';
|
|
144
158
|
// Back to fast retries for the next blip.
|
|
145
159
|
_reconnectAttempts = 0;
|
|
146
160
|
if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
|
|
161
|
+
// The drop took the old consumer down and every CollaborationChannel
|
|
162
|
+
// subscription made on it with it. Nothing else notices — cable
|
|
163
|
+
// availability is about support, not liveness — so the rooms would
|
|
164
|
+
// stay silently dead until each file was closed and reopened.
|
|
165
|
+
if (reconnected && typeof CollaborationService !== 'undefined' &&
|
|
166
|
+
typeof CollaborationService.rejoinRooms === 'function') {
|
|
167
|
+
try { CollaborationService.rejoinRooms(); } catch (e) { /* ignore */ }
|
|
168
|
+
}
|
|
147
169
|
},
|
|
148
170
|
disconnected: function () {
|
|
149
171
|
_status = _status === 'rejected' ? 'rejected' : 'dropped';
|
|
@@ -178,12 +200,71 @@ var WebSocketService = (function () {
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
// ── files_changed coalescing ───────────────────────────────────────────────
|
|
204
|
+
//
|
|
205
|
+
// The server broadcasts once per written file, and each subscriber turns a
|
|
206
|
+
// broadcast into HTTP work: the app refreshes the tree and git status, the
|
|
207
|
+
// Monaco plugin refreshes the TypeScript program, every open editor pane
|
|
208
|
+
// re-runs git line-diff. Saving several files in a row — paste, save, next
|
|
209
|
+
// file — therefore multiplied one write into a fan-out per save, and past
|
|
210
|
+
// the point where the dev server could keep up the queue simply grew: saves
|
|
211
|
+
// got slower and slower until requests hit the axios timeout.
|
|
212
|
+
//
|
|
213
|
+
// MbeditorApp already debounced its own handler for exactly this reason, but
|
|
214
|
+
// a debounce there cannot help the other subscribers. Coalescing here fixes
|
|
215
|
+
// every subscriber at once, including any added later.
|
|
216
|
+
var FILES_CHANGED_COALESCE_MS = 150;
|
|
217
|
+
var _filesChangedTimer = null;
|
|
218
|
+
var _filesChangedAcc = null;
|
|
219
|
+
|
|
220
|
+
// Merge a broadcast into the accumulator. Pure so it can be tested directly;
|
|
221
|
+
// the two invariants are easy to get wrong and both have teeth:
|
|
222
|
+
//
|
|
223
|
+
// - A broadcast with no paths means "something changed, re-check
|
|
224
|
+
// everything". It must not be narrowed by paths that happen to land in
|
|
225
|
+
// the same window, or the re-check silently covers only those files.
|
|
226
|
+
// - `structural` is a union: one structural broadcast in the burst makes
|
|
227
|
+
// the whole flush structural, because a tree walk that gets skipped
|
|
228
|
+
// leaves a created or deleted file missing from the explorer.
|
|
229
|
+
function _mergeFilesChanged(acc, data) {
|
|
230
|
+
var next = acc || { paths: [], pathless: false, structural: false };
|
|
231
|
+
if (data && data.paths) next.paths = next.paths.concat(data.paths);
|
|
232
|
+
else next.pathless = true;
|
|
233
|
+
// Absent `structural` means an older server — the conservative read is true.
|
|
234
|
+
if (!data || data.structural !== false) next.structural = true;
|
|
235
|
+
return next;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _coalescedPayload(acc) {
|
|
239
|
+
var payload = { type: 'files_changed', structural: acc.structural };
|
|
240
|
+
if (!acc.pathless && acc.paths.length) {
|
|
241
|
+
var seen = {};
|
|
242
|
+
payload.paths = acc.paths.filter(function (p) {
|
|
243
|
+
if (seen['$' + p]) return false;
|
|
244
|
+
seen['$' + p] = true;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return payload;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _flushFilesChanged() {
|
|
252
|
+
_filesChangedTimer = null;
|
|
253
|
+
var acc = _filesChangedAcc;
|
|
254
|
+
_filesChangedAcc = null;
|
|
255
|
+
if (!acc) return;
|
|
256
|
+
var payload = _coalescedPayload(acc);
|
|
257
|
+
_filesChangedCallbacks.slice().forEach(function (fn) {
|
|
258
|
+
try { fn(payload); } catch (e) { /* ignore */ }
|
|
184
259
|
});
|
|
185
260
|
}
|
|
186
261
|
|
|
262
|
+
function _emitFilesChanged(data) {
|
|
263
|
+
_filesChangedAcc = _mergeFilesChanged(_filesChangedAcc, data);
|
|
264
|
+
if (_filesChangedTimer) return;
|
|
265
|
+
_filesChangedTimer = setTimeout(_flushFilesChanged, FILES_CHANGED_COALESCE_MS);
|
|
266
|
+
}
|
|
267
|
+
|
|
187
268
|
function _emitFileSaved(data) {
|
|
188
269
|
_fileSavedCallbacks.forEach(function (fn) {
|
|
189
270
|
try { fn(data); } catch (e) { /* ignore */ }
|
|
@@ -342,6 +423,9 @@ var WebSocketService = (function () {
|
|
|
342
423
|
subscribeCollaboration: subscribeCollaboration,
|
|
343
424
|
perform: perform,
|
|
344
425
|
onFilesChanged: onFilesChanged,
|
|
426
|
+
// Exposed for tests — the burst-merge invariants above are worth pinning.
|
|
427
|
+
_mergeFilesChanged: _mergeFilesChanged,
|
|
428
|
+
_coalescedPayload: _coalescedPayload,
|
|
345
429
|
offFilesChanged: offFilesChanged,
|
|
346
430
|
onFileSaved: onFileSaved,
|
|
347
431
|
offFileSaved: offFileSaved,
|