mbeditor 0.11.0 → 0.12.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +193 -0
  3. data/README.md +190 -3
  4. data/app/assets/javascripts/mbeditor/application.js +5 -0
  5. data/app/assets/javascripts/mbeditor/application_iife_tail.js +6 -0
  6. data/app/assets/javascripts/mbeditor/collaboration_identity.js +234 -0
  7. data/app/assets/javascripts/mbeditor/collaboration_service.js +690 -0
  8. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +120 -19
  9. data/app/assets/javascripts/mbeditor/components/FileTree.js +127 -8
  10. data/app/assets/javascripts/mbeditor/components/GitPanel.js +12 -3
  11. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +127 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +916 -72
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +934 -0
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +130 -10
  15. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +1 -0
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +4 -2
  17. data/app/assets/javascripts/mbeditor/editor_plugins.js +517 -111
  18. data/app/assets/javascripts/mbeditor/file_import.js +146 -0
  19. data/app/assets/javascripts/mbeditor/file_service.js +52 -3
  20. data/app/assets/javascripts/mbeditor/tab_manager.js +50 -1
  21. data/app/assets/javascripts/mbeditor/websocket_service.js +89 -0
  22. data/app/assets/stylesheets/mbeditor/editor.css +365 -10
  23. data/app/channels/mbeditor/channel_authentication.rb +94 -0
  24. data/app/channels/mbeditor/collaboration_channel.rb +84 -0
  25. data/app/channels/mbeditor/editor_channel.rb +40 -1
  26. data/app/controllers/mbeditor/application_controller.rb +5 -1
  27. data/app/controllers/mbeditor/editors_controller.rb +492 -19
  28. data/app/controllers/mbeditor/git_controller.rb +9 -2
  29. data/app/services/mbeditor/availability_probe.rb +76 -17
  30. data/app/services/mbeditor/code_search_service.rb +23 -3
  31. data/app/services/mbeditor/collaboration_doc_store.rb +116 -0
  32. data/app/services/mbeditor/file_import_service.rb +103 -0
  33. data/app/services/mbeditor/git_combined_diff_service.rb +36 -5
  34. data/app/services/mbeditor/git_info_service.rb +6 -0
  35. data/app/services/mbeditor/git_service.rb +22 -6
  36. data/app/services/mbeditor/lsp_diagnostics_translator.rb +99 -5
  37. data/app/services/mbeditor/model_graph_service.rb +232 -0
  38. data/app/services/mbeditor/presence_registry.rb +83 -0
  39. data/app/services/mbeditor/ri_definition_service.rb +39 -5
  40. data/app/services/mbeditor/search_replace_service.rb +24 -4
  41. data/app/views/layouts/mbeditor/application.html.erb +2 -0
  42. data/lib/mbeditor/configuration.rb +37 -3
  43. data/lib/mbeditor/engine.rb +34 -0
  44. data/lib/mbeditor/exception_log.rb +84 -0
  45. data/lib/mbeditor/route_map.rb +5 -0
  46. data/lib/mbeditor/ruby_lsp_client.rb +28 -1
  47. data/lib/mbeditor/version.rb +1 -1
  48. data/lib/mbeditor.rb +1 -0
  49. data/vendor/assets/javascripts/yjs-collab.js +12 -0
  50. metadata +15 -2
@@ -0,0 +1,234 @@
1
+ // CollaborationIdentity — zero-config participant identity for live collaboration
2
+ // (slice 5/9, #56).
3
+ //
4
+ // Every browser gets a friendly display name and a stable colour with no setup:
5
+ // generated once, persisted in localStorage, and editable by the user. The host
6
+ // app can override the name through the `user_name_callback` config option — that
7
+ // resolved value arrives via /client_config and is applied with setServerName().
8
+ // Precedence for the effective name is: server name > user/stored name > generated.
9
+ //
10
+ // The colour is derived deterministically from the effective name against a fixed
11
+ // palette, so the same name always renders in the same colour and two peers get
12
+ // visibly distinct carets.
13
+ var CollaborationIdentity = (function () {
14
+ var STORAGE_KEY = 'mbeditor.collab.identity';
15
+
16
+ // Fixed palette — distinct, saturated hues that read against the dark editor
17
+ // background. Index chosen deterministically from the name (see _colorFor).
18
+ var PALETTE = [
19
+ '#e06c75', '#98c379', '#e5c07b', '#61afef', '#c678dd',
20
+ '#56b6c2', '#d19a66', '#be5046', '#7e9cff', '#3fb950'
21
+ ];
22
+
23
+ var ADJECTIVES = [
24
+ 'Swift', 'Calm', 'Bright', 'Bold', 'Quiet', 'Clever', 'Brave', 'Lucky',
25
+ 'Sunny', 'Witty', 'Nimble', 'Mellow', 'Eager', 'Jolly', 'Keen', 'Spry'
26
+ ];
27
+ var ROLES = [
28
+ 'Developer', 'Designer', 'Architect', 'Engineer', 'Coder', 'Hacker', 'Builder', 'Debugger',
29
+ 'Tester', 'Analyst', 'Maker', 'Pioneer', 'Wizard', 'Operator', 'Scripter', 'Tinkerer'
30
+ ];
31
+
32
+ var _serverName = null; // set by setServerName() from /client_config
33
+ var _stored = _load(); // { name, installId } persisted in the browser
34
+ var _listeners = []; // notified when the effective identity changes
35
+
36
+ // A presence participant is one tab/cable connection, so the client id is minted
37
+ // once per page load (NOT persisted — two tabs of the same profile are two
38
+ // distinct participants). Used to key the status-bar presence roster.
39
+ var _clientId = _mintClientId();
40
+
41
+ function _mintId() {
42
+ try {
43
+ if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
44
+ } catch (e) { /* fall through to Math.random */ }
45
+ return 'c-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
46
+ }
47
+
48
+ function _mintClientId() {
49
+ return _mintId();
50
+ }
51
+
52
+ function _load() {
53
+ var obj = null;
54
+ try {
55
+ var raw = window.localStorage.getItem(STORAGE_KEY);
56
+ if (raw) {
57
+ var parsed = JSON.parse(raw);
58
+ if (parsed && typeof parsed.name === 'string' && parsed.name) obj = parsed;
59
+ }
60
+ } catch (e) { /* storage unavailable / corrupt — fall through */ }
61
+
62
+ obj = obj || { name: _generateName() };
63
+ // Persistent per-browser id, distinct from the per-load client id. The colour
64
+ // is seeded from this so it survives a reload: seeding it from the client id
65
+ // meant every refresh handed you a new colour, and your peers watched your
66
+ // caret change hue for no reason. Backfilled for identities stored before
67
+ // this existed.
68
+ if (typeof obj.installId !== 'string' || !obj.installId) {
69
+ obj.installId = _mintId();
70
+ }
71
+ _persist(obj);
72
+ return obj;
73
+ }
74
+
75
+ function _persist(obj) {
76
+ try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(obj)); }
77
+ catch (e) { /* private mode / quota — identity still works in-memory */ }
78
+ }
79
+
80
+ function _generateName() {
81
+ var a = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
82
+ var n = ROLES[Math.floor(Math.random() * ROLES.length)];
83
+ return a + ' ' + n;
84
+ }
85
+
86
+ // Stable string hash (djb2) → palette index.
87
+ //
88
+ // Seeded on the persisted install id — not the name, and not the per-load client
89
+ // id. Both of those were tried and both were wrong in opposite directions:
90
+ // hashing the name gave two participants sharing a display name the same colour,
91
+ // making their carets indistinguishable exactly when you most need to tell them
92
+ // apart (and the generated name is one of only 128 adjective+role pairs, a host
93
+ // app's user_name_callback can legitimately return the same name twice, and two
94
+ // tabs of one browser share the stored name outright); hashing the client id
95
+ // fixed that but re-rolled the colour on every page load, so a reload changed
96
+ // your caret's hue in front of everyone you were pairing with.
97
+ // A peer's colour arrives from another machine and is interpolated into a
98
+ // stylesheet to draw their caret, so it is a trust boundary: without this,
99
+ // a colour of `red;} html{display:none} .x{` closes the rule and injects
100
+ // arbitrary CSS into your editor. Accept a hex literal or nothing.
101
+ var HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
102
+ var FALLBACK_COLOR = '#888888';
103
+
104
+ function safeColor(value) {
105
+ return HEX_COLOR.test(String(value || '')) ? String(value) : FALLBACK_COLOR;
106
+ }
107
+
108
+ function _hash(seed) {
109
+ var h = 5381;
110
+ for (var i = 0; i < seed.length; i++) h = ((h << 5) + h + seed.charCodeAt(i)) | 0;
111
+ return Math.abs(h);
112
+ }
113
+
114
+ function _colorFor(seed) {
115
+ return PALETTE[_hash(seed) % PALETTE.length];
116
+ }
117
+
118
+ function _effectiveName() {
119
+ if (_serverName) return _serverName;
120
+ return _stored.name;
121
+ }
122
+
123
+ var _color = _colorFor(_stored.installId);
124
+ // Published so a colour clash resolves the same way on both sides *and* the same
125
+ // way after a reload. Deriving it from the install id rather than sending the id
126
+ // itself keeps a persistent browser identifier off the wire; all the tie-break
127
+ // needs is a stable number to compare.
128
+ var _seed = _hash(_stored.installId);
129
+
130
+ function get() {
131
+ return { clientId: _clientId, name: _effectiveName(), color: _color, seed: _seed };
132
+ }
133
+
134
+ // Hashing alone still collides: 10 palette entries and 5 participants is a ~70%
135
+ // chance some pair matches. The colour has to be picked *against* the roster,
136
+ // which only exists after connecting — so mint from the hash, then reconcile
137
+ // here whenever the roster changes.
138
+ //
139
+ // Called by every client on the same roster, so the rule has to be one both
140
+ // sides of a clash compute identically or they swap forever: on a collision the
141
+ // higher seed yields and the lower keeps its colour. Two yielders can briefly
142
+ // grab the same free slot; the next roster change re-runs this and the same
143
+ // tie-break settles it, so it converges rather than oscillating.
144
+ //
145
+ // Ordered on the persisted seed, falling back to the per-load client id only
146
+ // when seeds match (two tabs of one browser, which share stored identity). Using
147
+ // the client id as the primary key would decide the yielder afresh on every
148
+ // reload, so a clashing pair swapped colours each time either of them refreshed
149
+ // — the same churn that seeding the colour per-load caused.
150
+ //
151
+ // peers: [{ clientId, color, seed }] — the current roster, excluding us.
152
+ function _yieldsTo(peer) {
153
+ var mine = _seed;
154
+ var theirs = typeof peer.seed === 'number' ? peer.seed : -1;
155
+ if (theirs !== mine) return theirs < mine;
156
+ return String(peer.clientId) < _clientId;
157
+ }
158
+
159
+ function reconcileColor(peers) {
160
+ if (!peers || !peers.length) return;
161
+
162
+ var clash = peers.some(function (p) {
163
+ return p.color === _color && _yieldsTo(p);
164
+ });
165
+ if (!clash) return;
166
+
167
+ var taken = {};
168
+ peers.forEach(function (p) { if (p.color) taken[p.color] = true; });
169
+ var free = PALETTE.filter(function (c) { return !taken[c]; });
170
+ // More participants than colours — a duplicate is unavoidable, so keep ours
171
+ // rather than churn. The hover card names who is who.
172
+ if (!free.length) return;
173
+
174
+ var next = free[_seed % free.length];
175
+ if (next === _color) return;
176
+ _color = next;
177
+ _notify();
178
+ }
179
+
180
+ function _notify() {
181
+ var id = get();
182
+ _listeners.forEach(function (cb) {
183
+ try { cb(id); } catch (e) { /* a bad listener must not break the others */ }
184
+ });
185
+ }
186
+
187
+ // Host-app override (from user_name_callback via /client_config). A blank value
188
+ // clears the override and falls back to the stored/generated name.
189
+ function setServerName(name) {
190
+ var next = (typeof name === 'string' && name.trim()) ? name.trim() : null;
191
+ if (next === _serverName) return;
192
+ _serverName = next;
193
+ _notify();
194
+ }
195
+
196
+ // User edit of their own display name. Persisted; clears any server override so
197
+ // the user's explicit choice wins for the rest of the session.
198
+ function setName(name) {
199
+ var clean = (typeof name === 'string') ? name.trim() : '';
200
+ if (!clean) return;
201
+ _stored = { name: clean };
202
+ _persist(_stored);
203
+ _serverName = null;
204
+ _notify();
205
+ }
206
+
207
+ // Prompt-based editor wired to the status-bar presence chip. Returns nothing;
208
+ // listeners pick up the change and push it onto live awareness.
209
+ function editName() {
210
+ var current = get().name;
211
+ var next = window.prompt('Your collaboration name', current);
212
+ if (next != null) setName(next);
213
+ }
214
+
215
+ // Subscribe to identity changes (name or colour). Returns an unsubscribe fn.
216
+ function onChange(cb) {
217
+ if (typeof cb !== 'function') return function () {};
218
+ _listeners.push(cb);
219
+ return function () {
220
+ var i = _listeners.indexOf(cb);
221
+ if (i !== -1) _listeners.splice(i, 1);
222
+ };
223
+ }
224
+
225
+ return {
226
+ get: get,
227
+ safeColor: safeColor,
228
+ setName: setName,
229
+ setServerName: setServerName,
230
+ reconcileColor: reconcileColor,
231
+ editName: editName,
232
+ onChange: onChange
233
+ };
234
+ })();