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
|
@@ -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
|
+
})();
|