mbeditor 0.12.9 → 0.13.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +216 -1
  3. data/app/assets/javascripts/mbeditor/application.js +6 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +52 -1
  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 +104 -103
  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 +224 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +704 -270
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +193 -9
  15. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +105 -77
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
  17. data/app/assets/javascripts/mbeditor/editor_plugins.js +434 -112
  18. data/app/assets/javascripts/mbeditor/file_import.js +78 -0
  19. data/app/assets/javascripts/mbeditor/file_service.js +112 -18
  20. data/app/assets/javascripts/mbeditor/git_service.js +130 -8
  21. data/app/assets/javascripts/mbeditor/history_service.js +33 -38
  22. data/app/assets/javascripts/mbeditor/log_service.js +4 -0
  23. data/app/assets/javascripts/mbeditor/search_service.js +50 -4
  24. data/app/assets/javascripts/mbeditor/tab_manager.js +66 -10
  25. data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
  26. data/app/assets/stylesheets/mbeditor/editor.css +389 -103
  27. data/app/channels/mbeditor/channel_authentication.rb +7 -0
  28. data/app/channels/mbeditor/collaboration_channel.rb +8 -2
  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 +155 -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 +49 -3
  35. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  36. data/app/services/mbeditor/editor_state_service.rb +36 -26
  37. data/app/services/mbeditor/exclusion_matcher.rb +12 -10
  38. data/app/services/mbeditor/file_operation_service.rb +38 -3
  39. data/app/services/mbeditor/file_tree_service.rb +30 -2
  40. data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
  41. data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
  42. data/app/services/mbeditor/git_diff_service.rb +5 -1
  43. data/app/services/mbeditor/git_info_service.rb +20 -8
  44. data/app/services/mbeditor/git_line_diff_service.rb +6 -2
  45. data/app/services/mbeditor/git_service.rb +65 -15
  46. data/app/services/mbeditor/js_definition_service.rb +3 -1
  47. data/app/services/mbeditor/js_globals_service.rb +18 -4
  48. data/app/services/mbeditor/js_members_service.rb +3 -2
  49. data/app/services/mbeditor/js_program_service.rb +15 -5
  50. data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
  51. data/app/services/mbeditor/process_runner.rb +42 -12
  52. data/app/services/mbeditor/ri_definition_service.rb +8 -1
  53. data/app/services/mbeditor/route_service.rb +45 -8
  54. data/app/services/mbeditor/rubocop_run_service.rb +98 -0
  55. data/app/services/mbeditor/ruby_definition_service.rb +23 -4
  56. data/app/services/mbeditor/schema_service.rb +73 -66
  57. data/app/services/mbeditor/search_replace_service.rb +42 -7
  58. data/app/services/mbeditor/test_runner_service.rb +45 -10
  59. data/lib/mbeditor/cable_log_filter.rb +8 -2
  60. data/lib/mbeditor/configuration.rb +4 -1
  61. data/lib/mbeditor/editor_bootstrap.rb +18 -12
  62. data/lib/mbeditor/engine.rb +22 -3
  63. data/lib/mbeditor/exception_log.rb +2 -2
  64. data/lib/mbeditor/pending_migrations.rb +33 -0
  65. data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
  66. data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
  67. data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
  68. data/lib/mbeditor/route_map.rb +1 -0
  69. data/lib/mbeditor/ruby_lsp_client.rb +71 -12
  70. data/lib/mbeditor/version.rb +1 -1
  71. data/lib/tasks/mbeditor.rake +23 -0
  72. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 264dfef604baf70be3fcb9984f588a93a8388fe13371cb3c8707d5ee7149d441
4
- data.tar.gz: d584056fbb108ea55ddb98a819af73e4eabb939d659fc7d54442446b34b2c203
3
+ metadata.gz: fe5dce956e01ff66387b9ccf99fc8b163ce131a226158eda7b6cf2c8513eaa09
4
+ data.tar.gz: 37e4f12f4ccd1ca7d209e7fe134a25d813bbe175862d53a470a5042aff9d7797
5
5
  SHA512:
6
- metadata.gz: 20141c5dcd65e482c4d0437c5b1a4b1431507f424dc1a31b80af5490d30b4611c2c2e050f0716a4a382c9f2b3804001127e03292cfe371b50049c6d058f3c7b0
7
- data.tar.gz: 551f8ad767679d4b63364d2c6e9bcb267663c35bf1750a355b29ff22f7149e445511a0fd9be8aa7a279754f6c0a3ccb79a1b3941d1bec3b11792911541081616
6
+ metadata.gz: 60c74bb23b92719d4273d720af42813e2ecd3d639281901d47037e332697819cbe58c338eea9aeff334f3533d48d2e62c994b2122fb36fbded4bef94bd225b36
7
+ data.tar.gz: 1451f3b2effb5d9cffd9811b41ea72b07d174702af9e6dc9993d5fbc92d990109dbf834e957c1eced22e8d77b815173d14bf6465b51072002a0e1c482745dc69
data/CHANGELOG.md CHANGED
@@ -5,7 +5,222 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.13.1] - 2026-08-26
9
+
10
+ ### Removed
11
+ - **The whole-suite test run is gone** — the status-bar "Tests" button, the
12
+ Test Run drawer, `POST /test_all`, and the `test_all_command` /
13
+ `test_all_timeout` settings. It was one blocking request holding a
14
+ development-server thread for the length of the suite, with no way to cancel
15
+ it and the runner's output buffered whole in the server process; on a real
16
+ host app that is twenty minutes of a frozen-looking button for a result the
17
+ terminal gives you faster and interruptibly. The per-file (and per-line) test
18
+ run stays — that one is scoped, finishes in seconds and is why the editor
19
+ runs tests at all. **A host app setting `test_all_command` or
20
+ `test_all_timeout` in its initializer must drop those lines.**
21
+ - **The ruby-lsp status-bar chip is gone**, along with its 10-second health
22
+ poll. ruby-lsp itself is untouched — definitions, hover, completion,
23
+ diagnostics and the rest all still run through it.
24
+
25
+ ### Added
26
+ - `rake mbeditor:scan_duplicates` — scans the workspace for files that contain a
27
+ duplicated copy of themselves, for finding files damaged before the seeding fix below.
28
+ Set `MBEDITOR_WORKSPACE_ROOT` to scan a tree other than the resolved workspace.
29
+
30
+ ### Fixed
31
+ - **Data loss: a saved file could contain two copies of itself.** When two editor
32
+ clients attached to a collaborative room the server had no state for — a fresh
33
+ room, or one evicted while both sat idle — each one seeded the shared document
34
+ from disk, and Yjs merged both inserts at offset 0 into two concatenated copies.
35
+ Saving then wrote the doubled buffer. The server now grants the right to seed a
36
+ room to exactly one client (`CollaborationDocStore.claim_seed`), never evicts a
37
+ room that still has subscribers, and a client whose cable reconnects re-publishes
38
+ its snapshot so a server that lost its state cannot hand out a second seed.
39
+ - **Status bar alignment.** Every item now sits in the same 8px cell instead of
40
+ carrying its own padding on top of the bar's (the branch, the offline chip
41
+ and the version each had a different one), the branch drops from 13px to the
42
+ bar's 11px, and `line-height: 1` on the bar *and* on its buttons puts an icon
43
+ and the number beside it in identical line boxes — Pico gives `<button>` its
44
+ own 1.5, so the problem counts sat in a 16.5px box next to an 11px icon.
45
+ - **The Problems panel's Fix button did nothing visible.** `rubocop -a` writes
46
+ its corrections to disk, but reports the offenses it just fixed alongside the
47
+ ones it left — each flagged both `corrected` and `correctable` — so the list
48
+ after a fix was byte-identical to the list before it and the button stayed
49
+ lit. Corrected offenses are now dropped from the response, open buffers are
50
+ re-read after the run, and clearing stale diagnostics now clears the React
51
+ marker map as well as Monaco's, which is what had left the panel, the tab
52
+ badges and the status-bar tallies disagreeing.
53
+ - **Quick Open matches loosely again.** MiniSearch was configured `fuzzy: false`,
54
+ so a single typo found nothing; fuzziness is now scaled per term. Folders sort
55
+ after files instead of ahead of every match, and the dialog and its text are
56
+ larger.
57
+ - **Search results read like VS Code's.** Hits group under a collapsible row per
58
+ file with a match count, the matched text is tinted with the accent colour and
59
+ is kept on screen at any panel width (eliding left and right around it), and
60
+ clicking a hit selects the match and leaves the cursor at its end.
61
+ - **Bulk import no longer opens on the previous import's folder** — the folder
62
+ you actually acted on is offered first — and it now reports what it wrote and
63
+ reveals the destination in the tree.
64
+ - `ReactRailsUJS` no longer reports as undefined; it ships inside the react-rails
65
+ gem, outside the workspace, so no static scan of the tree could ever find it.
66
+ - `<<~JSX` heredocs highlight as JavaScript (as do `<<~TSX`/`<<~TS`).
67
+ - The Open Editors list no longer runs its filenames underneath the split and
68
+ close buttons.
69
+
70
+ ### Changed
71
+ - **The activity bar is a third narrower** (60px to 40px), and the editor
72
+ toolbar's per-file Test button is gone; running the test under the cursor is
73
+ unaffected.
74
+
75
+ ## [0.13.0] - 2026-08-20
76
+
77
+ ### Added
78
+ - **Whole-suite test run, in a drawer.** `POST /test_all` runs the suite with
79
+ no file argument under its own `test_all_timeout` (30 min) — a suite run is
80
+ minutes, not seconds, and forcing `test_timeout` up to cover it would let one
81
+ hung example block for half an hour. It renders as a bottom drawer beside
82
+ Problems and the log rather than a modal: a backdrop that blocks the editor
83
+ for the length of a suite run turns the wait into dead time. Failures group
84
+ by file, each row jumps to its line, and "Open failing" opens them all at
85
+ once. The result survives closing the drawer and a reload, and a branch
86
+ switch drops it.
87
+ - **Whole-workspace RuboCop run in the Problems panel.** The panel could only
88
+ ever show diagnostics for files Monaco had open, because markers are
89
+ per-model. Three buttons now cover the rest of the project: RuboCop (a full
90
+ `rubocop --format json` run), Fix (`-a`, safe corrections only), and Open all.
91
+
92
+ ### Changed
93
+ - **Activity bar and explorer are larger.** Activity buttons go 36px to 60px
94
+ with a 25px glyph and fill the bar edge to edge, so the active indicator sits
95
+ flush at the screen edge; state is carried by glyph colour alone and the
96
+ hover box is gone. Explorer rows and icons go 12px to 14px.
97
+ - **Per-file test timeout raised 60s to 180s**, and the client now derives its
98
+ request timeout from the server's (`/workspace` reports both ceilings)
99
+ instead of a hard-coded 120s. Raising the server timeout past the client's
100
+ used to make the browser abort a run the server was still executing.
101
+
102
+ ### Security
103
+ - **Every entry point is gated on `allowed_environments`.** The check moved to
104
+ `ApplicationController` ahead of `run_authentication`. `LogsController`
105
+ declared no gate at all, so `GET /mbeditor/logs/tail` returned the tail of
106
+ `log/<env>.log` in any environment the engine was mounted in; both cable
107
+ channels checked only the (nil by default) authentication hook, so anything
108
+ reaching the host's `/cable` could subscribe and start a log tail or write
109
+ state. Running the host's auth proc now happens after the environment check,
110
+ so a disallowed environment 404s without it.
111
+ - **`model_schema` validates its parameter.** The model name flowed into a file
112
+ path without a format check, so `..` segments escaped the workspace.
113
+ - **Git ref patterns reject a leading dash**, and `git show` takes
114
+ `--end-of-options` — a ref beginning with `-` was argument injection into a
115
+ documented git option.
116
+
117
+ ### Performance
118
+ - **Typing no longer re-renders the whole application.** Every keystroke wrote
119
+ the full document into the store, which re-rendered an app subscribed to all
120
+ of it. Content writes are now throttled (250 ms, trailing) with a documented
121
+ flush-on-save contract, and the dirty flag is written only on a clean/dirty
122
+ transition. On top of that: the file tree is no longer deep-copied per render,
123
+ the model-graph scene (~10k SVG elements) no longer rebuilds when unrelated
124
+ state moves, and blame/test view zones, the tab strip's smooth scroll, and the
125
+ problem counts no longer churn per keypress.
126
+ - **Monaco's ruby-lsp providers honour cancellation.** `documentHighlight`
127
+ fires per cursor move and was POSTing the entire buffer with no way to cancel
128
+ it, so superseded requests stayed in flight; all eight raw providers now
129
+ thread Monaco's `CancellationToken` through one shared path, and highlights
130
+ memoise per word occurrence.
131
+ - **ruby-lsp requests pipeline again.** The document mutex was held across the
132
+ whole request/response round-trip, so a single diagnostics call (10 s budget)
133
+ or rename (30 s) blocked every hover and highlight in the process. It is now
134
+ held only across the write, which is what actually needs ordering. Abandoned
135
+ requests send `$/cancelRequest` rather than leaving a single-threaded server
136
+ computing a result nobody will read.
137
+ - **Conditional GETs on the polled and static routes.** Monaco's 14 MB of
138
+ assets carried no validators and were re-fetched on every page load; `/files`
139
+ shipped roughly 1 MB of JSON on a 10 s poll and now answers 304 from a digest
140
+ memoised with the existing tree cache. `/git_status`, hit every 5 s, is TTL
141
+ cached instead of spawning two git subprocesses per poll.
142
+ - **`find_branch_base` costs 2 git spawns instead of up to 12**, resolving the
143
+ candidate list with one `for-each-ref`. Commit detail drops from 3 spawns to
144
+ 2, and the line-diff probe only asks whether a file is untracked when the
145
+ diff came back empty.
146
+ - **A JS save no longer rebuilds the whole workspace JS program behind a global
147
+ lock**, and a `.rb` save no longer invalidates the JS program and globals
148
+ caches at all. Go-to-definition no longer rewrites the entire on-disk cache
149
+ (tens of MB of JSON) on any lookup that parsed something new.
150
+ - Smaller: one `lstat` per tree entry instead of three stats, unbounded
151
+ subprocess output capped, and the exclusion matcher splits each path once
152
+ rather than once per pattern.
153
+
154
+ ### Fixed
155
+ - **Pending migrations no longer lock the editor.** `CheckPending` is swapped
156
+ for a wrapper that runs the identical check for host traffic but records
157
+ rather than raises for the editor's, so the tree, opening a file and saving
158
+ one keep working — including saving the migration causing it. The warning
159
+ rides a response header, so it appears for a migration created mid-session
160
+ and clears once migrated without a reload. Host traffic is still blocked
161
+ exactly as Rails intends. The fallback shell also requested its Sprockets
162
+ assets under the engine mount, where they 404, and hardcoded `/mbeditor`
163
+ instead of the configured mount path.
164
+ - **Saves no longer degrade under a burst.** A timeout was counted as an
165
+ unreachable server, which suppressed the polls that would have proven it up;
166
+ `files_changed` is coalesced once at the WebSocket dispatch (15 writes: 15
167
+ dispatches to 1); panes re-ran `git diff` on every save anywhere in the
168
+ workspace; and the lite git poll escalated to the full fan-out twice per save
169
+ (12 saves: 12 fan-outs to 1).
170
+ - **Replace All left open tabs showing pre-replace text.** The refresh treated
171
+ a model-registry entry as a Monaco model, so the update silently no-opped
172
+ into a bare `catch` — and saving that tab afterwards wrote the stale content
173
+ back over the replacement.
174
+ - **A dropped cable connection disconnected the host application's own
175
+ ActionCable consumer** when the host provided one, taking its channels down
176
+ with it, and left collaboration rooms bound to the dead consumer with no
177
+ indication they were dead. Consumers are disconnected only when mbeditor
178
+ created them, and rooms rejoin on reconnect.
179
+ - **ruby-lsp failed entirely for any workspace path containing a space** or
180
+ other character needing escaping: the `file://` URI was built by
181
+ interpolation, so ruby-lsp's own `URI` parse raised and every request fell
182
+ back silently. Return-path comparisons unescape before matching the
183
+ workspace root.
184
+ - **Git status mangled paths with spaces, non-ASCII characters, or renames.**
185
+ Porcelain output was read verbatim, so quoted and octal-escaped paths arrived
186
+ unopenable and missed their numstat enrichment; both sides now use `-z`.
187
+ - **Saves and editor state are written atomically.** Both truncated in place,
188
+ so a reader landing mid-write parsed an empty file and silently reported no
189
+ open tabs, and a crash mid-write left the file empty for good. Writes go to a
190
+ temporary file and rename, resolving through symlinks so a linked file stays
191
+ a link.
192
+ - **Literal (non-regex) replace corrupted replacements containing
193
+ backslashes** — `\1`, `\&` and `\\` were interpreted as backreferences across
194
+ up to 500 files.
195
+ - **A user-supplied search regex could pin a worker indefinitely.** Patterns
196
+ are compiled with `Regexp.timeout` where the Ruby version supports it;
197
+ `Timeout.timeout` cannot interrupt a regex match, which is what the previous
198
+ guard relied on.
199
+ - **A failed ruby-lsp handshake orphaned the child process**, leaving it
200
+ indexing and holding memory, and up to three could accumulate.
201
+ - **Branch switches left stale buffers and diagnostics.** A checkout rewrites
202
+ the working tree and nothing told the editor; the git poll now announces it,
203
+ the app re-reads the tree and every open tab (dirty ones still prompt rather
204
+ than being clobbered), and `/files?refresh=1` bypasses the 15 s tree cache.
205
+ - **The Problems panel filled with vendored gem source** when a host
206
+ `.rubocop.yml` replaced RuboCop's default excludes, and reported TypeScript's
207
+ implicit-any advice on plain JS/JSX, where there are no annotations to add.
208
+ - **`HistoryService` rejected an empty base**, so the first history entry for a
209
+ file 400'd.
210
+ - Also: a V8 isolate leak in the JS syntax checker, unbounded growth in the
211
+ collaboration delta log and several per-path caches, JS member completions
212
+ cached with a stale insert range, `quick_fix`/format/haml-lint running with
213
+ no timeout, a negative log offset returning a 500, and configured JS globals
214
+ being dropped when the symbol cap was reached.
215
+
216
+ ### Tests
217
+ - The pending-migration middleware tests built `ActiveRecord::PendingMigrationError`
218
+ with no arguments, whose initializer lists the pending migrations and so needs
219
+ a database connection; every CI runner without one failed all six.
220
+ - Two stubs restored `AvailabilityProbe.rg` by rebinding a captured `Method`,
221
+ which raises `can't bind singleton method to a different class` if the class
222
+ is reloaded in between — leaving `rg` undefined and failing every later search
223
+ and workspace test in that process. They alias instead.
9
224
 
10
225
  ## [0.12.9] - 2026-08-07
11
226
 
@@ -33,6 +33,11 @@
33
33
  //= require mbeditor/components/GitPanel
34
34
  //= require mbeditor/components/QuickOpenDialog
35
35
  //= require mbeditor/components/TabBar
36
+ // Both import modals must be required *inside* the IIFE: React is an IIFE
37
+ // parameter (window.React is deliberately never set), so a component file
38
+ // placed after the tail resolves a bare `React` to undefined and takes the
39
+ // whole app down the first time it renders.
40
+ //= require mbeditor/components/ImportConflictModal
41
+ //= require mbeditor/components/ImportDialog
36
42
  //= require mbeditor/components/MbeditorApp
37
43
  //= require mbeditor/application_iife_tail
38
- //= require mbeditor/components/ImportConflictModal
@@ -225,6 +225,9 @@ var CollaborationService = (function () {
225
225
  binding: null,
226
226
  undoManager: null,
227
227
  lateJoin: false,
228
+ // Set from the sync handshake: the server grants the right to seed an empty
229
+ // room's shared doc from disk to exactly one client (see claim_seed).
230
+ seedGranted: false,
228
231
  onSeeded: null,
229
232
  attachRequested: false,
230
233
  degraded: false,
@@ -280,10 +283,14 @@ var CollaborationService = (function () {
280
283
  if (d) window.Y.applyUpdate(room.doc, _b64ToU8(d), REMOTE);
281
284
  });
282
285
  }
286
+ room.seedGranted = !!data.seed;
283
287
  room.synced = true;
284
288
  _maybeAttach(room);
285
289
  } else if (data.type === 'doc_update') {
286
290
  if (data.update) window.Y.applyUpdate(room.doc, _b64ToU8(data.update), REMOTE);
291
+ // A client that deferred attaching (empty room, seed granted to someone
292
+ // else) is waiting for exactly this: the seeder's content has landed.
293
+ _maybeAttach(room);
287
294
  } else if (data.type === 'awareness') {
288
295
  if (data.awareness && room.awareness && window.awarenessProtocol) {
289
296
  // REMOTE origin keeps the outbound handler from echoing peer state back.
@@ -295,6 +302,35 @@ var CollaborationService = (function () {
295
302
  }
296
303
  }
297
304
 
305
+ // Re-open every room's channel subscription on the current consumer.
306
+ //
307
+ // A cable drop tears the consumer down and takes every CollaborationChannel
308
+ // subscription with it — silently, because isAvailable() reports cable
309
+ // *support*, not liveness. Nothing here noticed: the rooms stayed in _rooms
310
+ // with dead subscriptions, so edits stopped reaching peers until the file was
311
+ // closed and reopened. WebSocketService calls this when the EditorChannel
312
+ // reconnects. Only the wire is replaced; the Yjs doc, the binding and the undo
313
+ // history all survive, and the re-sent sync snapshot re-applies harmlessly
314
+ // (Y.applyUpdate is idempotent).
315
+ function rejoinRooms() {
316
+ Object.keys(_rooms).forEach(function (path) {
317
+ var room = _rooms[path];
318
+ try { if (room.subscription) room.subscription.unsubscribe(); } catch (e) { /* already dead */ }
319
+ room.subscription = WebSocketService.subscribeCollaboration(path, {
320
+ // Awareness is never server-persisted, so peers lost our caret with the
321
+ // connection and would not see it again until we happened to move.
322
+ // The server may have lost the room entirely (restart): re-publish our
323
+ // content so the next opener late-joins instead of being granted a seed
324
+ // that would merge a second copy into what we still hold.
325
+ connected: function () {
326
+ _sendLocalAwareness(room);
327
+ if (room.binding) pushSnapshot(path);
328
+ },
329
+ received: function (data) { _onMessage(room, data); }
330
+ });
331
+ });
332
+ }
333
+
298
334
  // Register the current editor/model for a room and request a binding. Called on
299
335
  // every editor (re)creation (tab switch recreates the editor; the model and the
300
336
  // Yjs doc persist). opts.onSeeded is invoked once the binding has set the model,
@@ -323,6 +359,11 @@ var CollaborationService = (function () {
323
359
 
324
360
  function _maybeAttach(room) {
325
361
  if (room.binding || !room.synced || !room.attachRequested || !room.model) return;
362
+ // Shared doc still empty and the seed was granted to another client: its
363
+ // content is on its way. Binding now would hand the model to an empty Y.Text
364
+ // and wipe the buffer, so wait for the first doc_update (or the fallback
365
+ // timer, which lets the natively-loaded content stand).
366
+ if (room.text.length === 0 && !room.seedGranted && room.model.getValue().length > 0) return;
326
367
  _doAttach(room);
327
368
  }
328
369
 
@@ -330,10 +371,12 @@ var CollaborationService = (function () {
330
371
  var model = room.model;
331
372
  room.lateJoin = room.text.length > 0; // server already had shared state
332
373
 
333
- if (!room.lateJoin && model.getValue().length > 0) {
374
+ if (!room.lateJoin && room.seedGranted && model.getValue().length > 0) {
334
375
  // First opener whose disk content is already in the model: seed the shared
335
376
  // doc from it before constructing the binding, otherwise the binding would
336
377
  // overwrite the model to match the (empty) Y.Text and wipe the content.
378
+ // Only ever the client the SERVER picked — two clients each seeding an
379
+ // empty room merge into two concatenated copies of the file.
337
380
  room.doc.transact(function () { room.text.insert(0, model.getValue()); });
338
381
  }
339
382
 
@@ -561,6 +604,7 @@ var CollaborationService = (function () {
561
604
  // Client IDs are globally unique, so one shared <style> element covers all open
562
605
  // files. Re-runs on any awareness change (cheap; small participant counts).
563
606
  var _styleEl = null;
607
+ var _cursorCss = null;
564
608
  function _renderCursorStyles() {
565
609
  if (typeof document === 'undefined') return;
566
610
  if (!_styleEl) {
@@ -593,6 +637,12 @@ var CollaborationService = (function () {
593
637
  'font-family:sans-serif;z-index:10;pointer-events:none;}';
594
638
  });
595
639
  });
640
+ // Awareness changes on every caret move and every scroll frame, but the
641
+ // styles only depend on who is here and what colour they chose — which
642
+ // almost never changes. Re-assigning textContent invalidates the document's
643
+ // style rules, so skip the write when the CSS is identical.
644
+ if (css === _cursorCss) return;
645
+ _cursorCss = css;
596
646
  _styleEl.textContent = css;
597
647
  }
598
648
 
@@ -739,6 +789,7 @@ var CollaborationService = (function () {
739
789
  onAvailabilityChange: onAvailabilityChange,
740
790
  isEnabledFor: isEnabledFor,
741
791
  ensureRoom: ensureRoom,
792
+ rejoinRooms: rejoinRooms,
742
793
  bindEditor: bindEditor,
743
794
  unbindEditor: unbindEditor,
744
795
  leaveRoom: leaveRoom,
@@ -87,6 +87,11 @@
87
87
  function provideDocumentColors(model) {
88
88
  var colors = [];
89
89
  var lineCount = model.getLineCount();
90
+ // Registered for every language and re-run on every change, so a big file
91
+ // pays two regex passes per line for every keystroke. Same threshold
92
+ // EditorPanel's applyLargeFileTuning uses to switch off the other
93
+ // whole-document decorations.
94
+ if (lineCount > 2500) return [];
90
95
  for (var ln = 1; ln <= lineCount; ln++) {
91
96
  var line = model.getLineContent(ln);
92
97
  if (line.length > 2000) continue; // skip pathological lines
@@ -49,7 +49,9 @@ var CollapsibleSection = function CollapsibleSection(_ref) {
49
49
  icon && React.createElement("i", { className: "collapsible-icon " + icon }),
50
50
  React.createElement(
51
51
  "span",
52
- { className: "collapsible-title" },
52
+ // The title truncates away entirely on a narrow sidebar, so it carries
53
+ // its own tooltip — otherwise the section becomes unidentifiable.
54
+ { className: "collapsible-title", title: typeof title === 'string' ? title : undefined },
53
55
  title
54
56
  ),
55
57
  actions && React.createElement(