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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b65424322b66f78929de0629db6e4644a76467e889adaee77121b8c52e0e50de
|
|
4
|
+
data.tar.gz: 53cd7724f2a80011d474a9616bdb35d88859ddf797a0fa51d3657740038624bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4470580292e46d6de7eb2e3687193452bfbcbee5d478e48c6460e22739fd2942d1a525b04e6d3f7877175db5d18da86c3241032dfc748ca91bf62f0f008cd90
|
|
7
|
+
data.tar.gz: 05e4226ce7151a7e72d18f34018c30acc04adfbaea235a891afe12ecadb52170b1b598820ea7de326068eae7bb7811cce13ec5cf42b0c69fd6c77683c3b7c819
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,213 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.0] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Whole-suite test run, in a drawer.** `POST /test_all` runs the suite with
|
|
14
|
+
no file argument under its own `test_all_timeout` (30 min) — a suite run is
|
|
15
|
+
minutes, not seconds, and forcing `test_timeout` up to cover it would let one
|
|
16
|
+
hung example block for half an hour. It renders as a bottom drawer beside
|
|
17
|
+
Problems and the log rather than a modal: a backdrop that blocks the editor
|
|
18
|
+
for the length of a suite run turns the wait into dead time. Failures group
|
|
19
|
+
by file, each row jumps to its line, and "Open failing" opens them all at
|
|
20
|
+
once. The result survives closing the drawer and a reload, and a branch
|
|
21
|
+
switch drops it.
|
|
22
|
+
- **Whole-workspace RuboCop run in the Problems panel.** The panel could only
|
|
23
|
+
ever show diagnostics for files Monaco had open, because markers are
|
|
24
|
+
per-model. Three buttons now cover the rest of the project: RuboCop (a full
|
|
25
|
+
`rubocop --format json` run), Fix (`-a`, safe corrections only), and Open all.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **Activity bar and explorer are larger.** Activity buttons go 36px to 60px
|
|
29
|
+
with a 25px glyph and fill the bar edge to edge, so the active indicator sits
|
|
30
|
+
flush at the screen edge; state is carried by glyph colour alone and the
|
|
31
|
+
hover box is gone. Explorer rows and icons go 12px to 14px.
|
|
32
|
+
- **Per-file test timeout raised 60s to 180s**, and the client now derives its
|
|
33
|
+
request timeout from the server's (`/workspace` reports both ceilings)
|
|
34
|
+
instead of a hard-coded 120s. Raising the server timeout past the client's
|
|
35
|
+
used to make the browser abort a run the server was still executing.
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
- **Every entry point is gated on `allowed_environments`.** The check moved to
|
|
39
|
+
`ApplicationController` ahead of `run_authentication`. `LogsController`
|
|
40
|
+
declared no gate at all, so `GET /mbeditor/logs/tail` returned the tail of
|
|
41
|
+
`log/<env>.log` in any environment the engine was mounted in; both cable
|
|
42
|
+
channels checked only the (nil by default) authentication hook, so anything
|
|
43
|
+
reaching the host's `/cable` could subscribe and start a log tail or write
|
|
44
|
+
state. Running the host's auth proc now happens after the environment check,
|
|
45
|
+
so a disallowed environment 404s without it.
|
|
46
|
+
- **`model_schema` validates its parameter.** The model name flowed into a file
|
|
47
|
+
path without a format check, so `..` segments escaped the workspace.
|
|
48
|
+
- **Git ref patterns reject a leading dash**, and `git show` takes
|
|
49
|
+
`--end-of-options` — a ref beginning with `-` was argument injection into a
|
|
50
|
+
documented git option.
|
|
51
|
+
|
|
52
|
+
### Performance
|
|
53
|
+
- **Typing no longer re-renders the whole application.** Every keystroke wrote
|
|
54
|
+
the full document into the store, which re-rendered an app subscribed to all
|
|
55
|
+
of it. Content writes are now throttled (250 ms, trailing) with a documented
|
|
56
|
+
flush-on-save contract, and the dirty flag is written only on a clean/dirty
|
|
57
|
+
transition. On top of that: the file tree is no longer deep-copied per render,
|
|
58
|
+
the model-graph scene (~10k SVG elements) no longer rebuilds when unrelated
|
|
59
|
+
state moves, and blame/test view zones, the tab strip's smooth scroll, and the
|
|
60
|
+
problem counts no longer churn per keypress.
|
|
61
|
+
- **Monaco's ruby-lsp providers honour cancellation.** `documentHighlight`
|
|
62
|
+
fires per cursor move and was POSTing the entire buffer with no way to cancel
|
|
63
|
+
it, so superseded requests stayed in flight; all eight raw providers now
|
|
64
|
+
thread Monaco's `CancellationToken` through one shared path, and highlights
|
|
65
|
+
memoise per word occurrence.
|
|
66
|
+
- **ruby-lsp requests pipeline again.** The document mutex was held across the
|
|
67
|
+
whole request/response round-trip, so a single diagnostics call (10 s budget)
|
|
68
|
+
or rename (30 s) blocked every hover and highlight in the process. It is now
|
|
69
|
+
held only across the write, which is what actually needs ordering. Abandoned
|
|
70
|
+
requests send `$/cancelRequest` rather than leaving a single-threaded server
|
|
71
|
+
computing a result nobody will read.
|
|
72
|
+
- **Conditional GETs on the polled and static routes.** Monaco's 14 MB of
|
|
73
|
+
assets carried no validators and were re-fetched on every page load; `/files`
|
|
74
|
+
shipped roughly 1 MB of JSON on a 10 s poll and now answers 304 from a digest
|
|
75
|
+
memoised with the existing tree cache. `/git_status`, hit every 5 s, is TTL
|
|
76
|
+
cached instead of spawning two git subprocesses per poll.
|
|
77
|
+
- **`find_branch_base` costs 2 git spawns instead of up to 12**, resolving the
|
|
78
|
+
candidate list with one `for-each-ref`. Commit detail drops from 3 spawns to
|
|
79
|
+
2, and the line-diff probe only asks whether a file is untracked when the
|
|
80
|
+
diff came back empty.
|
|
81
|
+
- **A JS save no longer rebuilds the whole workspace JS program behind a global
|
|
82
|
+
lock**, and a `.rb` save no longer invalidates the JS program and globals
|
|
83
|
+
caches at all. Go-to-definition no longer rewrites the entire on-disk cache
|
|
84
|
+
(tens of MB of JSON) on any lookup that parsed something new.
|
|
85
|
+
- Smaller: one `lstat` per tree entry instead of three stats, unbounded
|
|
86
|
+
subprocess output capped, and the exclusion matcher splits each path once
|
|
87
|
+
rather than once per pattern.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
- **Pending migrations no longer lock the editor.** `CheckPending` is swapped
|
|
91
|
+
for a wrapper that runs the identical check for host traffic but records
|
|
92
|
+
rather than raises for the editor's, so the tree, opening a file and saving
|
|
93
|
+
one keep working — including saving the migration causing it. The warning
|
|
94
|
+
rides a response header, so it appears for a migration created mid-session
|
|
95
|
+
and clears once migrated without a reload. Host traffic is still blocked
|
|
96
|
+
exactly as Rails intends. The fallback shell also requested its Sprockets
|
|
97
|
+
assets under the engine mount, where they 404, and hardcoded `/mbeditor`
|
|
98
|
+
instead of the configured mount path.
|
|
99
|
+
- **Saves no longer degrade under a burst.** A timeout was counted as an
|
|
100
|
+
unreachable server, which suppressed the polls that would have proven it up;
|
|
101
|
+
`files_changed` is coalesced once at the WebSocket dispatch (15 writes: 15
|
|
102
|
+
dispatches to 1); panes re-ran `git diff` on every save anywhere in the
|
|
103
|
+
workspace; and the lite git poll escalated to the full fan-out twice per save
|
|
104
|
+
(12 saves: 12 fan-outs to 1).
|
|
105
|
+
- **Replace All left open tabs showing pre-replace text.** The refresh treated
|
|
106
|
+
a model-registry entry as a Monaco model, so the update silently no-opped
|
|
107
|
+
into a bare `catch` — and saving that tab afterwards wrote the stale content
|
|
108
|
+
back over the replacement.
|
|
109
|
+
- **A dropped cable connection disconnected the host application's own
|
|
110
|
+
ActionCable consumer** when the host provided one, taking its channels down
|
|
111
|
+
with it, and left collaboration rooms bound to the dead consumer with no
|
|
112
|
+
indication they were dead. Consumers are disconnected only when mbeditor
|
|
113
|
+
created them, and rooms rejoin on reconnect.
|
|
114
|
+
- **ruby-lsp failed entirely for any workspace path containing a space** or
|
|
115
|
+
other character needing escaping: the `file://` URI was built by
|
|
116
|
+
interpolation, so ruby-lsp's own `URI` parse raised and every request fell
|
|
117
|
+
back silently. Return-path comparisons unescape before matching the
|
|
118
|
+
workspace root.
|
|
119
|
+
- **Git status mangled paths with spaces, non-ASCII characters, or renames.**
|
|
120
|
+
Porcelain output was read verbatim, so quoted and octal-escaped paths arrived
|
|
121
|
+
unopenable and missed their numstat enrichment; both sides now use `-z`.
|
|
122
|
+
- **Saves and editor state are written atomically.** Both truncated in place,
|
|
123
|
+
so a reader landing mid-write parsed an empty file and silently reported no
|
|
124
|
+
open tabs, and a crash mid-write left the file empty for good. Writes go to a
|
|
125
|
+
temporary file and rename, resolving through symlinks so a linked file stays
|
|
126
|
+
a link.
|
|
127
|
+
- **Literal (non-regex) replace corrupted replacements containing
|
|
128
|
+
backslashes** — `\1`, `\&` and `\\` were interpreted as backreferences across
|
|
129
|
+
up to 500 files.
|
|
130
|
+
- **A user-supplied search regex could pin a worker indefinitely.** Patterns
|
|
131
|
+
are compiled with `Regexp.timeout` where the Ruby version supports it;
|
|
132
|
+
`Timeout.timeout` cannot interrupt a regex match, which is what the previous
|
|
133
|
+
guard relied on.
|
|
134
|
+
- **A failed ruby-lsp handshake orphaned the child process**, leaving it
|
|
135
|
+
indexing and holding memory, and up to three could accumulate.
|
|
136
|
+
- **Branch switches left stale buffers and diagnostics.** A checkout rewrites
|
|
137
|
+
the working tree and nothing told the editor; the git poll now announces it,
|
|
138
|
+
the app re-reads the tree and every open tab (dirty ones still prompt rather
|
|
139
|
+
than being clobbered), and `/files?refresh=1` bypasses the 15 s tree cache.
|
|
140
|
+
- **The Problems panel filled with vendored gem source** when a host
|
|
141
|
+
`.rubocop.yml` replaced RuboCop's default excludes, and reported TypeScript's
|
|
142
|
+
implicit-any advice on plain JS/JSX, where there are no annotations to add.
|
|
143
|
+
- **`HistoryService` rejected an empty base**, so the first history entry for a
|
|
144
|
+
file 400'd.
|
|
145
|
+
- Also: a V8 isolate leak in the JS syntax checker, unbounded growth in the
|
|
146
|
+
collaboration delta log and several per-path caches, JS member completions
|
|
147
|
+
cached with a stale insert range, `quick_fix`/format/haml-lint running with
|
|
148
|
+
no timeout, a negative log offset returning a 500, and configured JS globals
|
|
149
|
+
being dropped when the symbol cap was reached.
|
|
150
|
+
|
|
151
|
+
### Tests
|
|
152
|
+
- The pending-migration middleware tests built `ActiveRecord::PendingMigrationError`
|
|
153
|
+
with no arguments, whose initializer lists the pending migrations and so needs
|
|
154
|
+
a database connection; every CI runner without one failed all six.
|
|
155
|
+
- Two stubs restored `AvailabilityProbe.rg` by rebinding a captured `Method`,
|
|
156
|
+
which raises `can't bind singleton method to a different class` if the class
|
|
157
|
+
is reloaded in between — leaving `rg` undefined and failing every later search
|
|
158
|
+
and workspace test in that process. They alias instead.
|
|
159
|
+
|
|
160
|
+
## [0.12.9] - 2026-08-07
|
|
161
|
+
|
|
162
|
+
### Added
|
|
163
|
+
- **Problems panel severity filter.** Three toggle chips — errors, warnings,
|
|
164
|
+
info — each showing its live count, filtering the list independently of the
|
|
165
|
+
text filter. Multi-select rather than one-of-three, so "errors and warnings
|
|
166
|
+
but not the convention noise" is expressible. The selection persists, and the
|
|
167
|
+
last active severity stays latched on so the panel can't be filtered down to
|
|
168
|
+
an unexplained blank.
|
|
169
|
+
- **Search results carry match columns.** Clicking a result now puts the cursor
|
|
170
|
+
at the end of the matched text instead of the start of the line, ready to
|
|
171
|
+
type. Columns are measured against the raw source line on the server —
|
|
172
|
+
the row's display text is stripped, so a column derived from it would land
|
|
173
|
+
short by the indent on every indented hit.
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
- **The Problems header no longer restates its counts in prose.** The severity
|
|
177
|
+
chips carry the same three numbers.
|
|
178
|
+
- **Paste re-indents instead of reformatting.** Pasting ran the pasted range
|
|
179
|
+
through Prettier, which reprinted the whole enclosing statement — and for a
|
|
180
|
+
paste that filled the document, every line of it — so pasting a snippet
|
|
181
|
+
rewrote code you never touched. Paste now only re-indents to the file's
|
|
182
|
+
indentation setting; quotes, semicolons and spacing come through exactly as
|
|
183
|
+
copied. The setting is renamed "Format on paste" → "Indent on paste"
|
|
184
|
+
(`indentOnPaste`, still on by default). Monaco ships no indentation rules for
|
|
185
|
+
JavaScript, which would have made the re-indent a silent no-op there, so
|
|
186
|
+
VS Code's JS rules are now registered. Format-on-save is unchanged and still
|
|
187
|
+
off by default.
|
|
188
|
+
|
|
189
|
+
### Fixed
|
|
190
|
+
- **Go-to-definition on a JS symbol defined in the same file opened a junk tab
|
|
191
|
+
named after a number.** Models are created without an explicit URI, so Monaco
|
|
192
|
+
identifies each as `inmemory://model/N`; the TS worker returns that URI for
|
|
193
|
+
an in-file definition, and the editor opener stripped it to a path and opened
|
|
194
|
+
a phantom tab called "57". Only `file://` resources are treated as workspace
|
|
195
|
+
files now — the rest go back to Monaco, which reveals the position in the
|
|
196
|
+
current editor.
|
|
197
|
+
- **Clicking a search result sometimes landed on the wrong line.** Opening a
|
|
198
|
+
hit in a file that wasn't already open ran the jump against a still-empty
|
|
199
|
+
model: it clamped to line 1 and then cleared the pending jump, so when the
|
|
200
|
+
content finally arrived nothing re-triggered it. Whether it misbehaved came
|
|
201
|
+
down to whether the fetch beat a 50 ms timer — hence "sometimes". The jump
|
|
202
|
+
now waits for the content, cancels a superseded timer, and clamps to the
|
|
203
|
+
file's real length when a result outlives the line it pointed at.
|
|
204
|
+
- **Search results ignored created and deleted files.** Only saves dropped the
|
|
205
|
+
client-side search cache, so a search re-run after adding or removing a file
|
|
206
|
+
was served the stale cached page. Every structural mutation now invalidates
|
|
207
|
+
it and re-runs the active query.
|
|
208
|
+
- **Search and the git status counts lagged behind file changes.** The
|
|
209
|
+
live-result refresh sat behind a 2 s debounce (now 250 ms), and every save
|
|
210
|
+
fired the full `/git_info` fan-out — the most expensive request the editor
|
|
211
|
+
makes — twice over, once directly and once from the broadcast handler, which
|
|
212
|
+
on a dev server with a few threads queued the tree and search requests behind
|
|
213
|
+
it. Saves and file mutations now use the cheap `/git_status` probe, which
|
|
214
|
+
patches the branch and file list immediately and escalates to the fan-out
|
|
215
|
+
itself only when the branch actually changed.
|
|
216
|
+
|
|
10
217
|
## [0.12.8] - 2026-08-07
|
|
11
218
|
|
|
12
219
|
### Added
|
|
@@ -27,12 +27,18 @@
|
|
|
27
27
|
//= require mbeditor/components/TestResultsPanel
|
|
28
28
|
//= require mbeditor/components/LogPanel
|
|
29
29
|
//= require mbeditor/components/ProblemsPanel
|
|
30
|
+
//= require mbeditor/components/TestRunPanel
|
|
30
31
|
//= require mbeditor/components/CodeReviewPanel
|
|
31
32
|
//= require mbeditor/components/EditorPanel
|
|
32
33
|
//= require mbeditor/components/FileTree
|
|
33
34
|
//= require mbeditor/components/GitPanel
|
|
34
35
|
//= require mbeditor/components/QuickOpenDialog
|
|
35
36
|
//= require mbeditor/components/TabBar
|
|
37
|
+
// Both import modals must be required *inside* the IIFE: React is an IIFE
|
|
38
|
+
// parameter (window.React is deliberately never set), so a component file
|
|
39
|
+
// placed after the tail resolves a bare `React` to undefined and takes the
|
|
40
|
+
// whole app down the first time it renders.
|
|
41
|
+
//= require mbeditor/components/ImportConflictModal
|
|
42
|
+
//= require mbeditor/components/ImportDialog
|
|
36
43
|
//= require mbeditor/components/MbeditorApp
|
|
37
44
|
//= require mbeditor/application_iife_tail
|
|
38
|
-
//= require mbeditor/components/ImportConflictModal
|
|
@@ -295,6 +295,29 @@ var CollaborationService = (function () {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// Re-open every room's channel subscription on the current consumer.
|
|
299
|
+
//
|
|
300
|
+
// A cable drop tears the consumer down and takes every CollaborationChannel
|
|
301
|
+
// subscription with it — silently, because isAvailable() reports cable
|
|
302
|
+
// *support*, not liveness. Nothing here noticed: the rooms stayed in _rooms
|
|
303
|
+
// with dead subscriptions, so edits stopped reaching peers until the file was
|
|
304
|
+
// closed and reopened. WebSocketService calls this when the EditorChannel
|
|
305
|
+
// reconnects. Only the wire is replaced; the Yjs doc, the binding and the undo
|
|
306
|
+
// history all survive, and the re-sent sync snapshot re-applies harmlessly
|
|
307
|
+
// (Y.applyUpdate is idempotent).
|
|
308
|
+
function rejoinRooms() {
|
|
309
|
+
Object.keys(_rooms).forEach(function (path) {
|
|
310
|
+
var room = _rooms[path];
|
|
311
|
+
try { if (room.subscription) room.subscription.unsubscribe(); } catch (e) { /* already dead */ }
|
|
312
|
+
room.subscription = WebSocketService.subscribeCollaboration(path, {
|
|
313
|
+
// Awareness is never server-persisted, so peers lost our caret with the
|
|
314
|
+
// connection and would not see it again until we happened to move.
|
|
315
|
+
connected: function () { _sendLocalAwareness(room); },
|
|
316
|
+
received: function (data) { _onMessage(room, data); }
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
298
321
|
// Register the current editor/model for a room and request a binding. Called on
|
|
299
322
|
// every editor (re)creation (tab switch recreates the editor; the model and the
|
|
300
323
|
// Yjs doc persist). opts.onSeeded is invoked once the binding has set the model,
|
|
@@ -561,6 +584,7 @@ var CollaborationService = (function () {
|
|
|
561
584
|
// Client IDs are globally unique, so one shared <style> element covers all open
|
|
562
585
|
// files. Re-runs on any awareness change (cheap; small participant counts).
|
|
563
586
|
var _styleEl = null;
|
|
587
|
+
var _cursorCss = null;
|
|
564
588
|
function _renderCursorStyles() {
|
|
565
589
|
if (typeof document === 'undefined') return;
|
|
566
590
|
if (!_styleEl) {
|
|
@@ -593,6 +617,12 @@ var CollaborationService = (function () {
|
|
|
593
617
|
'font-family:sans-serif;z-index:10;pointer-events:none;}';
|
|
594
618
|
});
|
|
595
619
|
});
|
|
620
|
+
// Awareness changes on every caret move and every scroll frame, but the
|
|
621
|
+
// styles only depend on who is here and what colour they chose — which
|
|
622
|
+
// almost never changes. Re-assigning textContent invalidates the document's
|
|
623
|
+
// style rules, so skip the write when the CSS is identical.
|
|
624
|
+
if (css === _cursorCss) return;
|
|
625
|
+
_cursorCss = css;
|
|
596
626
|
_styleEl.textContent = css;
|
|
597
627
|
}
|
|
598
628
|
|
|
@@ -739,6 +769,7 @@ var CollaborationService = (function () {
|
|
|
739
769
|
onAvailabilityChange: onAvailabilityChange,
|
|
740
770
|
isEnabledFor: isEnabledFor,
|
|
741
771
|
ensureRoom: ensureRoom,
|
|
772
|
+
rejoinRooms: rejoinRooms,
|
|
742
773
|
bindEditor: bindEditor,
|
|
743
774
|
unbindEditor: unbindEditor,
|
|
744
775
|
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
|
-
|
|
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(
|
|
@@ -6,6 +6,7 @@ var _React = React;
|
|
|
6
6
|
var useState = _React.useState;
|
|
7
7
|
var useEffect = _React.useEffect;
|
|
8
8
|
var useRef = _React.useRef;
|
|
9
|
+
var useMemo = _React.useMemo;
|
|
9
10
|
|
|
10
11
|
var IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp', 'bmp', 'avif'];
|
|
11
12
|
|
|
@@ -611,7 +612,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
611
612
|
var _histBranch = EditorStore.getState().gitBranch || '';
|
|
612
613
|
if (_histBranch) {
|
|
613
614
|
if (_reusingModel) {
|
|
614
|
-
HistoryService.resumeTracking(_histBranch, tab.path);
|
|
615
|
+
HistoryService.resumeTracking(_histBranch, tab.path, modelObj.getValue());
|
|
615
616
|
} else {
|
|
616
617
|
HistoryService.beginTracking(_histBranch, tab.path, tab.content || '');
|
|
617
618
|
}
|
|
@@ -647,9 +648,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
647
648
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
648
649
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
649
650
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
// the same editorPrefs.formatOnPaste setting.
|
|
651
|
+
// Paste inserts what was copied, unchanged. See attachEditorFeatures for
|
|
652
|
+
// why re-indenting it was removed rather than repaired.
|
|
653
653
|
formatOnPaste: false,
|
|
654
654
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
655
655
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
@@ -1213,10 +1213,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1213
1213
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
1214
1214
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
1215
1215
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
formatOnPaste: false,
|
|
1216
|
+
// Paste inserts what was copied, unchanged. See attachEditorFeatures
|
|
1217
|
+
// for why re-indenting it was removed rather than repaired.
|
|
1218
|
+
formatOnPaste: false,
|
|
1220
1219
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1221
1220
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
1222
1221
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
@@ -1301,22 +1300,40 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1301
1300
|
return function() { window.removeEventListener('mbeditor:focusPane', onFocusPane); };
|
|
1302
1301
|
}, [paneId]);
|
|
1303
1302
|
|
|
1304
|
-
// Jump to line if specified
|
|
1303
|
+
// Jump to line if specified.
|
|
1304
|
+
//
|
|
1305
|
+
// The wait for content is load-bearing, not defensive. Opening a search hit
|
|
1306
|
+
// in a file that wasn't already open runs this effect once while the model
|
|
1307
|
+
// is still empty: the jump clamped to line 1 and then cleared gotoLine, so
|
|
1308
|
+
// when the content finally arrived there was nothing left to re-trigger it
|
|
1309
|
+
// and the cursor stayed on the wrong line. Whether it misbehaved came down
|
|
1310
|
+
// to whether the fetch beat a 50 ms timer — hence "sometimes".
|
|
1305
1311
|
useEffect(function () {
|
|
1306
|
-
if (tab.gotoLine
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1312
|
+
if (!tab.gotoLine || !monacoRef.current) return;
|
|
1313
|
+
if (tab.loading) return;
|
|
1314
|
+
|
|
1315
|
+
var editor = monacoRef.current;
|
|
1316
|
+
var timer = setTimeout(function () {
|
|
1317
|
+
var model = editor.getModel();
|
|
1318
|
+
if (!model || model.isDisposed()) return;
|
|
1319
|
+
|
|
1320
|
+
// A result can outlive the line it pointed at (the file shrank since the
|
|
1321
|
+
// scan). Clamp instead of asking Monaco for a line that isn't there.
|
|
1322
|
+
var line = Math.max(1, Math.min(tab.gotoLine, model.getLineCount()));
|
|
1323
|
+
var column = tab.gotoCol || 1;
|
|
1324
|
+
var maxColumn = model.getLineMaxColumn(line);
|
|
1325
|
+
if (column > maxColumn) column = maxColumn;
|
|
1326
|
+
|
|
1327
|
+
editor.revealLineInCenter(line);
|
|
1328
|
+
editor.setPosition({ lineNumber: line, column: column });
|
|
1329
|
+
editor.focus();
|
|
1330
|
+
|
|
1331
|
+
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
1332
|
+
TabManager.clearGotoLine(paneId, tab.path);
|
|
1333
|
+
}, 50);
|
|
1334
|
+
|
|
1335
|
+
return function () { clearTimeout(timer); };
|
|
1336
|
+
}, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
|
|
1320
1337
|
|
|
1321
1338
|
// Apply RuboCop markers
|
|
1322
1339
|
useEffect(function () {
|
|
@@ -1477,8 +1494,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1477
1494
|
gitLineRefreshRef.current = refresh;
|
|
1478
1495
|
|
|
1479
1496
|
// Immediate path for writes mbeditor made itself.
|
|
1497
|
+
//
|
|
1498
|
+
// Only when *this* file is one of the ones that changed: saving some other
|
|
1499
|
+
// file cannot move this file's diff against HEAD. Refreshing regardless
|
|
1500
|
+
// meant every open pane ran a git diff on every save anywhere in the
|
|
1501
|
+
// workspace, so the cost of a save scaled with how many editors were open.
|
|
1502
|
+
// A broadcast that names no paths still refreshes — that is the "something
|
|
1503
|
+
// happened, re-check everything" case (a commit, a branch switch), which
|
|
1504
|
+
// genuinely can change this file's diff without touching it.
|
|
1480
1505
|
var hasSocket = typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged;
|
|
1481
|
-
var onChanged = hasSocket ? function () {
|
|
1506
|
+
var onChanged = hasSocket ? function (payload) {
|
|
1507
|
+
if (payload && payload.paths && payload.paths.indexOf(tab.path) === -1) return;
|
|
1508
|
+
refresh();
|
|
1509
|
+
} : null;
|
|
1482
1510
|
if (onChanged) WebSocketService.onFilesChanged(onChanged);
|
|
1483
1511
|
|
|
1484
1512
|
return function () {
|
|
@@ -1554,6 +1582,16 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1554
1582
|
//
|
|
1555
1583
|
// Decorations, not view zones: a hint belongs on the line, and a zone would
|
|
1556
1584
|
// push the code around every time a file was opened.
|
|
1585
|
+
//
|
|
1586
|
+
// Redrawn whenever the text changes, not only when the file is opened. A
|
|
1587
|
+
// decoration is a zero-width range pinned at the end of the `def` line, and
|
|
1588
|
+
// Monaco keeps that position through an edit rather than re-deriving it — so
|
|
1589
|
+
// replacing a method (a paste over the selection is the usual way) left the
|
|
1590
|
+
// hint at a column that now falls in the middle of whatever text took its
|
|
1591
|
+
// place, rendering as `def sho GET /orders/:idw`. Nothing recomputed it,
|
|
1592
|
+
// so saving did not clear it either; it survived until the file was
|
|
1593
|
+
// reopened. The route data itself only depends on the route set, so an edit
|
|
1594
|
+
// redraws from the cached response without asking the server again.
|
|
1557
1595
|
useEffect(function () {
|
|
1558
1596
|
var editor = monacoRef.current;
|
|
1559
1597
|
if (!editor || !window.monaco) return;
|
|
@@ -1563,17 +1601,19 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1563
1601
|
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, []);
|
|
1564
1602
|
};
|
|
1565
1603
|
|
|
1566
|
-
if (
|
|
1604
|
+
if (editorPrefs.routeHints === false ||
|
|
1605
|
+
!tab.path || !/^app\/controllers\/.+_controller\.rb$/.test(tab.path)) {
|
|
1567
1606
|
clear();
|
|
1568
1607
|
return;
|
|
1569
1608
|
}
|
|
1570
1609
|
|
|
1571
1610
|
var cancelled = false;
|
|
1572
|
-
|
|
1611
|
+
var redrawTimer = null;
|
|
1612
|
+
|
|
1613
|
+
var draw = function (actions) {
|
|
1573
1614
|
if (cancelled) return;
|
|
1574
|
-
var actions = (data && data.actions) || {};
|
|
1575
1615
|
var model = editor.getModel();
|
|
1576
|
-
if (!model) return;
|
|
1616
|
+
if (!model || model.isDisposed()) return;
|
|
1577
1617
|
|
|
1578
1618
|
var decorations = [];
|
|
1579
1619
|
var text = model.getValue().split('\n');
|
|
@@ -1616,10 +1656,47 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1616
1656
|
});
|
|
1617
1657
|
|
|
1618
1658
|
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, decorations);
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
var contentSub = null;
|
|
1662
|
+
FileService.getRoutes(tab.path).then(function (data) {
|
|
1663
|
+
if (cancelled) return;
|
|
1664
|
+
var actions = (data && data.actions) || {};
|
|
1665
|
+
draw(actions);
|
|
1666
|
+
|
|
1667
|
+
// Debounced: a redraw per keystroke would rebuild every decoration in
|
|
1668
|
+
// the file for an edit that usually cannot have moved one.
|
|
1669
|
+
var model = editor.getModel();
|
|
1670
|
+
if (!model || !model.onDidChangeContent) return;
|
|
1671
|
+
contentSub = model.onDidChangeContent(function () {
|
|
1672
|
+
clearTimeout(redrawTimer);
|
|
1673
|
+
redrawTimer = setTimeout(function () { draw(actions); }, 250);
|
|
1674
|
+
});
|
|
1619
1675
|
}).catch(function () { /* hints are additive; a failure just means none */ });
|
|
1620
1676
|
|
|
1621
|
-
return function () {
|
|
1622
|
-
|
|
1677
|
+
return function () {
|
|
1678
|
+
cancelled = true;
|
|
1679
|
+
clearTimeout(redrawTimer);
|
|
1680
|
+
if (contentSub) contentSub.dispose();
|
|
1681
|
+
clear();
|
|
1682
|
+
};
|
|
1683
|
+
}, [tab.path, tab.externalContentVersion, monacoReady, editorPrefs.routeHints]);
|
|
1684
|
+
|
|
1685
|
+
// Blame and test view zones are anchored to line numbers, so they only need
|
|
1686
|
+
// rebuilding once an edit has settled. Both effects used to list tab.content
|
|
1687
|
+
// directly, which tore down and re-created every zone in the file on every
|
|
1688
|
+
// keystroke. This ticks 250 ms after the last change instead — the same
|
|
1689
|
+
// treatment the route-hint redraw above already gets.
|
|
1690
|
+
var _useStateZT = useState(0);
|
|
1691
|
+
var _useStateZT2 = _slicedToArray(_useStateZT, 2);
|
|
1692
|
+
var zoneTick = _useStateZT2[0];
|
|
1693
|
+
var setZoneTick = _useStateZT2[1];
|
|
1694
|
+
useEffect(function () {
|
|
1695
|
+
var timer = setTimeout(function () {
|
|
1696
|
+
setZoneTick(function (n) { return n + 1; });
|
|
1697
|
+
}, 250);
|
|
1698
|
+
return function () { clearTimeout(timer); };
|
|
1699
|
+
}, [tab.content]);
|
|
1623
1700
|
|
|
1624
1701
|
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
1625
1702
|
useEffect(function () {
|
|
@@ -1707,8 +1784,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1707
1784
|
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
1708
1785
|
}
|
|
1709
1786
|
|
|
1710
|
-
//
|
|
1711
|
-
}, [blameData, isBlameVisible, tab.id,
|
|
1787
|
+
// zoneTick, not tab.content: the zones follow the content, 250 ms behind it.
|
|
1788
|
+
}, [blameData, isBlameVisible, tab.id, zoneTick]);
|
|
1712
1789
|
|
|
1713
1790
|
// Check whether the current tab is the source file for the test that was run.
|
|
1714
1791
|
// e.g. testPanelFile = "test/controllers/theme_controller_test.rb"
|
|
@@ -1786,6 +1863,14 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1786
1863
|
return null;
|
|
1787
1864
|
};
|
|
1788
1865
|
|
|
1866
|
+
// treeHasPath walks the whole file tree once per candidate, and the Test
|
|
1867
|
+
// button asked for this on every render — so once per keystroke. treeData is
|
|
1868
|
+
// deliberately outside the React.memo comparator below, so it can only change
|
|
1869
|
+
// on a render this component was going to do anyway.
|
|
1870
|
+
var testFileForTab = useMemo(function () {
|
|
1871
|
+
return matchingTestFilePath(tab.path);
|
|
1872
|
+
}, [tab.path, treeData]);
|
|
1873
|
+
|
|
1789
1874
|
// Map a test method name to the best-matching line in the source file.
|
|
1790
1875
|
// Extracts keywords from the test name and scores each source line.
|
|
1791
1876
|
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
@@ -1822,9 +1907,17 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1822
1907
|
var model = editor.getModel();
|
|
1823
1908
|
var lineCount = model ? model.getLineCount() : 0;
|
|
1824
1909
|
|
|
1825
|
-
|
|
1910
|
+
// Which test file a result entry belongs to. A whole-suite run spans many
|
|
1911
|
+
// of them and stamps each entry with its own `file`, so there is no single
|
|
1912
|
+
// testPanelFile to gate on; a per-file run leaves `file` unset on the
|
|
1913
|
+
// passing entries and testPanelFile is the answer for those.
|
|
1914
|
+
var testFileFor = function (t) { return (t && t.file) || testPanelFile; };
|
|
1915
|
+
var relevantTests = ((testResult && testResult.tests) || []).filter(function (t) {
|
|
1916
|
+
var file = testFileFor(t);
|
|
1917
|
+
return file && tab.path && isSourceForTest(tab.path, file);
|
|
1918
|
+
});
|
|
1826
1919
|
|
|
1827
|
-
if (!testResult || !testInlineVisible ||
|
|
1920
|
+
if (!testResult || !testInlineVisible || relevantTests.length === 0) {
|
|
1828
1921
|
clearTestZones(editor);
|
|
1829
1922
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1830
1923
|
return;
|
|
@@ -1835,10 +1928,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1835
1928
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1836
1929
|
|
|
1837
1930
|
var normPath = function(p) { return p ? p.replace(/^\/+/, '') : ''; };
|
|
1838
|
-
var viewingTestFile = normPath(tab.path) === normPath(testPanelFile);
|
|
1839
1931
|
|
|
1840
|
-
var
|
|
1841
|
-
var testsWithStatus = tests.filter(function (t) {
|
|
1932
|
+
var testsWithStatus = relevantTests.filter(function (t) {
|
|
1842
1933
|
return t.status === 'pass' || t.status === 'fail' || t.status === 'error';
|
|
1843
1934
|
});
|
|
1844
1935
|
|
|
@@ -1847,6 +1938,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1847
1938
|
// Determine line number for each test
|
|
1848
1939
|
var sourceContent = tab.content || '';
|
|
1849
1940
|
var mapped = testsWithStatus.map(function(t) {
|
|
1941
|
+
// Per entry, not per panel: in a suite run this tab is the test file
|
|
1942
|
+
// for some entries and the source counterpart for others.
|
|
1943
|
+
var viewingTestFile = normPath(tab.path) === normPath(testFileFor(t));
|
|
1850
1944
|
var line;
|
|
1851
1945
|
if (viewingTestFile && t.line && t.line >= 1 && t.line <= lineCount) {
|
|
1852
1946
|
line = t.line;
|
|
@@ -1905,8 +1999,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1905
1999
|
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1906
2000
|
}
|
|
1907
2001
|
|
|
1908
|
-
//
|
|
1909
|
-
|
|
2002
|
+
// zoneTick, not tab.content — see the blame effect. The effect still reads
|
|
2003
|
+
// tab.content for mapTestToSourceLine; the tick only decides when it re-runs.
|
|
2004
|
+
}, [testResult, testInlineVisible, testPanelFile, tab.id, tab.path, zoneTick]);
|
|
1910
2005
|
|
|
1911
2006
|
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
1912
2007
|
var markdownContent = tab.isPreview ? sourceTab && sourceTab.content || tab.content || '' : tab.content || '';
|
|
@@ -2235,7 +2330,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2235
2330
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2236
2331
|
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
|
|
2237
2332
|
),
|
|
2238
|
-
testAvailable &&
|
|
2333
|
+
testAvailable && testFileForTab && React.createElement(
|
|
2239
2334
|
'button',
|
|
2240
2335
|
{
|
|
2241
2336
|
className: 'ide-icon-btn',
|
|
@@ -423,7 +423,12 @@ var FileTree = function FileTree(_ref) {
|
|
|
423
423
|
return result;
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
// A per-level localeCompare sort of the whole visible tree, re-run on every
|
|
427
|
+
// render — including the ones caused by a git poll or a tab switch, neither
|
|
428
|
+
// of which can change the shape of the tree. Only these three inputs can.
|
|
429
|
+
var flatItems = useMemo(function () {
|
|
430
|
+
return flattenVisible(items || [], 0, '');
|
|
431
|
+
}, [items, expandedDirs, pendingCreate]);
|
|
427
432
|
flatItemsRef.current = flatItems;
|
|
428
433
|
|
|
429
434
|
var totalHeight = flatItems.length * ROW_HEIGHT;
|