# Objective
Opening a diff against a heavily drifted branch sits on a spinner until
every changed file has loaded. Blob reads were serialized through the
git job queue and enqueued in `open_buffer` completion order rather than
path order, so the blob for the first excerpt could be hundreds of jobs
deep.
## Solution
1. `load_blob_content` skips the job queue — blobs are
content-addressed, so the read doesn't depend on the status snapshot.
2. `DiffBuffer::load` is a lazy future instead of an eagerly-spawned
`Task`, so the consumer picks the concurrency.
3. `DiffMultibuffer` drives those loads through `buffered(16)`.
All three are needed: dropping the queue without bounding forks
thousands of `cat-file` processes, and bounding without dropping it
still serializes.
Trust handling is unaffected, since the untrusted-repo flags are applied
in `GitBinary::build_command` rather than by the queue.
Worth noting that `handle_get_blob_content` is remote-reachable, so a
guest can now put up to 16 `cat-file` processes on the host instead of
queueing them.
## Testing
Added `project_diff::tests::test_excerpts_are_ordered_by_path`, which
pins excerpt order to `PathKey` rather than to load completion order,
using more files than the concurrency limit and several executor seeds.
Green: `cargo test -p git_ui --lib` (131), `cargo test -p project --lib
git_store` (11), `./script/clippy`.
Not tested against a remote or collab project, which is where the
`handle_get_blob_content` change actually matters.
### Numbers
Zed repo against a base ~3000 commits back: 2346 changed files, 1794
with a blob at the merge base. Warm cache, one `cat-file` process per
blob, best of two runs at each concurrency.
| concurrency | total | per blob | speedup |
|---|---|---|---|
| 1 (previous behaviour) | 77.25s | 43.1ms | 1.00x |
| 2 | 17.74s | 9.9ms | 4.35x |
| 4 | 10.05s | 5.6ms | 7.69x |
| 8 | 7.34s | 4.1ms | 10.52x |
| 16 (this change) | 6.63s | 3.7ms | 11.65x |
| 32 | 6.59s | 3.7ms | 11.73x |
| 64 | 7.14s | 4.0ms | 10.82x |
16 is where the curve flattens: 32 buys another 0.7% and 64 regresses,
so there's nothing above it worth the extra in-flight buffers or the
extra concurrent processes on a host serving a remote peer.
At one blob at a time the first excerpt waits at an arbitrary position
in a 1794-deep queue, so roughly 38s on average, which lines up with the
45s full load in the recording below. It's now in the first batch.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments — none added
- [x] The content adheres to Zed's UI standards — no UI changes
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
Opening a branch diff against a base ~3000 commits back, 2346 changed
files. Both clips are cut to 15s to keep the file size down — the before
run took 45s to finish loading.
**Before**
https://github.com/user-attachments/assets/d9475de8-ddd3-48a0-99e2-042a2f655b42
**After**
https://github.com/user-attachments/assets/0bf0e73d-0fac-4c2a-9749-71fee376f260
---
Release Notes:
- Improved how quickly a branch diff shows its first changes when the
branch has drifted a long way from its base
Also updates CI to always run this - that way we ensure that disabling
or enabling tests does not become stale at some point and thus can
safely add this.
Release Notes:
- N/A
- [GPUI] N/A
Zed did not advertise document highlight dynamic registration support or
handle `textDocument/documentHighlight` registrations. Dynamically
registered providers therefore fell through to the unhandled-capability
path, while servers with a fallback had to advertise a static provider
for every attached document.
Advertise dynamic registration support and track document highlight
registrations and unregistrations alongside the other selector-aware
dynamic text document capabilities. Refresh editors whenever the
effective document highlight registration state changes, including
selector-only changes that leave the merged provider unchanged, so
runtime registrations take effect immediately and unregistrations clear
stale or in-flight highlights.
Add coverage for the client capability, registration lifecycle with
multiple selectors sharing identical provider options, and editor
refresh behavior when an applicable selector is added or removed.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed’s UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
---------
Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
# Objective
Files matching the `private_files` setting are meant not to be shared
with collaborators. That is true when a guest asks to open one directly,
`respond_to_open_buffer_request` refuses with `ErrorCode::UnsharedItem`.
Project search reaches the same `create_buffer_for_peer` without passing
through that check. In `handle_search_candidate_buffers`, every matching
buffer was shared with the requesting peer unconditionally, so a guest
searching for a string that happens to appear in a private file received
the buffer containing it.
## Solution
Check `is_private` before sharing a matched buffer with a peer
## Testing
added new test `test_project_search_excludes_private_files` in the
collab integration suite:"
The host configures `private_files: ["**/.env"]` and has two files
`.env` containing `API_KEY=secret` and `a.txt` containing `the secret is
out`. The guest joins the shared project and searches for `secret`.
---
Release Notes:
- Fixed private files being shared with collaborators through project
search.
Before this change, two different guests requesting the same kind of
request would cancel each other's requests and only the latest one would
have arrived to the latest to query that.
Release Notes:
- Fixed collab LSP requests cancelling each other sometimes
Zed previously discarded the documentSelector from dynamically
registered textDocument capabilities and treated those capabilities as
applicable to every buffer attached to the server. This could route
requests for documents outside a registration selector.
Track dynamic textDocument registrations by method and registration ID,
and use their selectors when routing local requests, checking
capabilities, and deriving completion triggers. Static capabilities
continue to apply to every attached buffer. Language and URI scheme
matching are supported, while unsupported pattern matching remains
fail-open.
Keep document color, symbol, link, code lens, and folding range caches
aligned with the selector-aware server set so incapable or non-matching
servers do not leave caches perpetually incomplete.
Require an advertised diagnostic provider before sending pull diagnostic
requests, and add integration coverage for selector-aware dynamic
completion registration and trigger updates.
---
Release Notes:
- Improved language server request routing to respect dynamically
registered document selectors.
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
The language registry and LSP log store are shared by every workspace in
a Zed process. The LSP Logs selector enumerated the shared log store
without filtering by project, while each activity indicator consumed
global binary status updates. This exposed servers from other windows as
"Unknown worktree" entries and surfaced their startup failures or
timeouts in unrelated status bars.
Key each logged server by its owning project or LSP store together with
its process-local language server ID. Use that identity consistently
when adding, removing, streaming, selecting, opening, and reselecting
log entries so local, remote, SSH, and supplementary servers cannot
collide across ownership domains.
Model Copilot and other servers outside the project LSP store as
project-scoped supplementary servers, while matching SSH servers by
their owning LSP store.
Tag project-originated binary status updates with the owning LSP store
entity ID. Carry that source through extension worktree delegates and
every worktree-based WASM language-server call, then let each local LSP
store forward only statuses for its project. Activity indicators rely
solely on their project LSP store, and remote projects continue to
receive host statuses through the existing protocol path instead of
subscribing to local registry events.
Own each binary-status subscription task in its local LSP store and
unregister its broadcaster sender when the store is dropped, so closing
windows cannot leave detached status tasks or stale senders behind.
Add regression coverage for duplicate process-local server IDs across
ownership domains, filtering remote, supplementary, and SSH servers from
other projects, retaining servers owned by the current project or LSP
store, preserving extension status sources, and unregistering dropped
status subscriptions.
Release Notes:
- Fixed language servers from other projects appearing in the LSP Logs
view and their statuses showing in unrelated windows' status bars
LSP 3.18 allows servers to send [`MarkupContent` diagnostic
messages](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#diagnostic),
guarded by the client capability
`textDocument.diagnostic.markupMessageSupport`.
- Advertise `markup_message_support` only while pull diagnostics are
enabled because `textDocument.diagnostic` is the pull-model capability.
LSP 3.18 currently has no equivalent capability for push diagnostics:
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#diagnosticClientCapabilitieshttps://github.com/microsoft/language-server-protocol/issues/2246
- Handle `string | MarkupContent` messages in both push and pull
diagnostic paths: markdown-kind messages populate the existing
`Diagnostic::markdown` field (rendered in hover popovers and the
diagnostics panel), plaintext-kind markup remains plain text, and plain
string messages keep going through the adapter's
`diagnostic_message_to_markdown`.
- Preserve server-provided markup for code actions sent back to the
originating server, avoid forwarding markup diagnostics to other
servers, and render diagnostic metadata without altering the
server-provided markdown.
- Carry the markup kind over RPC via a new optional
`LspDiagnostic.markup_message_kind` proto field so remote/collab
projects round-trip `MarkupContent` messages.
Depends on the lsp-types change that turns `Diagnostic.message` into a
`DiagnosticMessage` enum (`string | MarkupContent`):
https://github.com/zed-industries/lsp-types/pull/15
Once merged, Cargo.toml can point to the upstream commit.
Note that Zed already rendered some plain-string diagnostics as Markdown
through language-specific adapter hooks, including `rust-analyzer`. This
adds protocol-level support for diagnostics explicitly sent as LSP 3.18
`MarkupContent`, while preserving the existing adapter fallbacks.
As a result, this does not introduce a new diagnostics rendering
pipeline.
Server-provided Markdown is routed through the existing
`Diagnostic::markdown` path, with small fixes to handle Markdown-only
updates and block content correctly.
## Showcase
Example from [JETLS.jl](github.com/aviatesk/JETLS.jl) (a Julia language
server):
> Before
<img width="924" height="160" alt="Screenshot 2026-07-15 at 16 05 40"
src="https://github.com/user-attachments/assets/b19fe9e3-50e3-44e4-9b4d-ac8bd438d3d6"
/>
> After
<img width="924" height="160" alt="Screenshot 2026-07-15 at 14 13 05"
src="https://github.com/user-attachments/assets/359c3382-118d-4fa6-aa91-6bd3661d3d28"
/>
---
Release Notes:
- Added support for rendering LSP 3.18 Markdown diagnostic messages from
language servers.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
# Objective
Remove avoidable clones of loop-invariant values that pay a copy on
every loop iteration. These were found by running a prototype
`deep_clone_in_loop` dylint (modeled on the accidentally quadratic
project-search fix in #62658) across the workspace. None of these sites
are quadratic in practice — the loops are bounded — but each one does
strictly wasted work that a one-line change removes.
## Solution
- `collab/src/api/extensions.rs`: use `next_marker.take()` instead of
`next_marker.clone()` for the S3 pagination marker. The marker is either
overwritten via `clone_from` at the end of the iteration or the loop
breaks, so it is never read after the `take`.
- `project/src/manifest_tree/path_trie.rs`: build the trie key with
`Arc::from(path_so_far.as_rel_path())` instead of
`path_so_far.clone().into()`. The `From<RelPathBuf> for Arc<RelPath>`
impl copies the bytes into the `Arc` anyway, so the intermediate
`RelPathBuf` clone was a second, redundant copy.
- `context_server/src/client.rs`: in `notify`, split off the last
handler and move the `serde_json::Value` payload into it, cloning only
for the preceding handlers. Previously every handler received a clone
and the final one was always wasted.
- `editor/src/rewrap.rs`: use `std::mem::replace` to move the
comment-delimiter and rewrap-prefix accumulators into the pushed range
instead of cloning them and immediately reassigning.
## Testing
- `cargo check -p collab -p project -p context_server -p editor` —
clean.
- `cargo test -p editor rewrap` — 7/7 pass, including the
boundary-sensitive `test_rewrap*` editor tests.
- `cargo test -p context_server` — 97/97 pass.
- `cargo test -p project path_trie` — 2/2 pass.
- Re-ran the `deep_clone_in_loop` lint on the four crates: the
`extensions.rs`, `path_trie.rs`, and `rewrap.rs` hits are gone;
`client.rs` still (correctly) reports the remaining required per-handler
clone.
- Not tested platform-specifically; the changes are
platform-independent.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- N/A
# Objective
- Add file-level hyperlink support for opening and copying hyperlinks
for Git remote source control to project panel and tabs
- Unify the language for Git remote hyperlinks across blame, project
panels, and tabs to be similar to GitLens' definitions (which are
commonly used in VS Code)
## Solution
- Updated the Git crate to expose relevant metadata to surface new
options within the editor
- Added new options for `Open File on Remote` and `Copy Remote File URL`
to project panel/file explorer and file tabs
- Altered the blame UI options to be consistent with new labels / more
clear
## Testing
- Ran `cargo run` to boot app and manually smoke test (see proof below)
- Added unit tests for this change
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
https://github.com/user-attachments/assets/d600ca4f-47e7-438c-90e1-f40124a606b4
---
Release Notes:
- Added options to open or copy whole-file permalinks from file tabs and
the Project Panel.
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Closes#62796. Follow-up to #62110.
# Objective
The range of a diagnostic entry is anchored when it is ingested, so it
follows edits. The ranges in the related information kept on the
diagnostic were the ones the server published. A code action request
carried both and reported the same note at two different lines: the
entry Zed flattened that note into had followed the edit, the related
information had not.
The distance does not correct itself either. When diagnostics are merged
rather than replaced, the existing entries are re-collected from their
anchors while the payload is cloned as it is, so the two positions drift
further apart with every edit that passes.
`mlir-lsp-server` shows what this costs. `MLIRTextFile::getCodeActions`
takes the line number out of `relatedInformation`, and
`getCodeActionForDiagnostic` resolves it against its own current
document, reading that line to copy its indentation before inserting the
`expected-note` check.
## Solution
The related information moves from `Diagnostic` onto
`DiagnosticEntry<T>`, next to the range it belongs with. The locations
of the diagnostic's own file are then in the same coordinate space as
that range: anchors inside the buffer, points in the worktree's store.
`Diagnostic` carries no coordinates again, so nothing that is only
meaningful inside one buffer travels with a payload that outlives it.
Every transition goes through `DiagnosticEntry::map_coordinates`: the
unsaved-edit adjustment and the clipping when diagnostics are ingested,
the anchoring in `DiagnosticSet::new`, and the conversion back to points
in `merge_diagnostic_entries`. Only the diagnostic's own range is
widened when it is empty, since that is for how it is rendered, while
the related locations are reported back as the server framed them.
Locations in another file have nothing here to anchor to and are kept as
published, which is also what `mlir-lsp-server` expects, since it skips
them.
`DiagnosticEntryRef` is left alone. It is what the rendering path
iterates, down to the scrollbar markers that walk every diagnostic of
the buffer on each frame, so nothing there converts or allocates. The
entries are read through `diagnostic_entries_in_range` where the request
is built, and `diagnostics_in_range` is now implemented on top of it.
The field is an `Option`, as an empty `Arc<[_]>` still allocates and
most diagnostics carry no related information.
`data` has the same staleness and cannot be anchored, as it is opaque.
## Commits
The third commit is mechanical: it introduces `DiagnosticEntry::new` and
rewrites the literals at its call sites, so that the last commit holds
only the change of behaviour.
## Testing
Three tests, all failing before this change. The first two are added as
separate commits, so that they can be run against `main`:
- `test_code_actions_related_information_follows_edits` edits above the
note and requests code actions, where the two positions for it used to
disagree.
- `test_code_actions_related_information_drifts_across_merges` edits and
pulls diagnostics twice, where the distance used to be every line
inserted since the diagnostic was published rather than the last edit
alone.
- `test_code_actions_related_information_of_disk_based_diagnostics`
publishes a diagnostic computed against the file on disk while the
buffer holds an unsaved edit, covering the adjustment the two positions
share.
- `cargo test -p project`
- `cargo test -p language -p diagnostics -p editor`
- `cargo fmt --all -- --check`
- `./script/clippy -p project -p language`
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed language servers receiving outdated positions for the related
information of a diagnostic when code actions are requested.
## Objective
`file_scan_exclusions` replaces the defaults instead of adding to them,
so excluding one extra directory means restating all eleven default
globs and never picking up defaults added in later Zed releases.
## Solution
`file_scan_exclusions` now accepts the `"..."` entry, which expands to
the value it overrides, so `["**/node_modules", "..."]` adds to the
inherited globs instead of replacing them. Entries listed by name keep
their position, and leaving `"..."` out still replaces the list
outright, so existing settings behave exactly as they do today.
## Testing
- Four unit tests in `crates/settings_content/src/project.rs` cover
splicing versus replacing, accumulation across successive layers, and
edge cases: a repeated `"..."`, an empty list clearing the value, and a
bare `["..."]` leaving it unchanged.
- To check by hand: set `"file_scan_exclusions": ["**/node_modules",
"..."]` in user settings and confirm `node_modules` disappears from the
project panel and file finder while `.git` and `.DS_Store` stay
excluded. Remove `"..."` and confirm only `node_modules` is excluded.
Repeat in a project's `.zed/settings.json` to confirm it splices the
resolved user settings rather than the defaults.
- Tested on macOS. This is platform-independent settings-merge logic
with no OS-specific code paths, so I did not test Linux or Windows.
## Self-Review Checklist:
- [x] I’ve reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed’s UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Added support for the `"..."` entry in `file_scan_exclusions`. Custom
exclusions can now extend the defaults instead of replacing them.
## Summary
Fixes#56449.
Related to #16965.
Zed’s Git panel and Project Diff build UI diffs from `language::Buffer`
diff bases loaded through the Git backend. Git blob loading previously
converted bytes with `String::from_utf8(...).ok()`, so legacy-encoded
blobs were treated as missing and the whole worktree file appeared newly
added.
This follows the same encoding path used for worktree buffers:
- move shared byte decoding and encoding into `language`
- keep Git blob, revision, and index APIs byte-oriented with `Vec<u8>`
- decode diff bases and index contents in `GitStore`, where
`language::Buffer`s are created
- encode index writes using the open buffer’s encoding and BOM so
partial staging does not rewrite the file as UTF-8
- keep worktree loading and saving on the same shared implementation
Regression coverage includes Windows-1251 decoding/encoding,
UTF-8/UTF-16 BOM preservation, raw Windows-1251 Git blob loading, and a
`BufferDiffSnapshot` assertion that a one-line CP1251 edit produces one
modified-line hunk instead of a full-file rewrite.
This does not run Git `textconv` commands. It fixes the reported
legacy-encoding case without executing repository-configured commands or
modifying working files on disk.
## Testing
- `cargo test -p language file_content::tests --locked`
- `cargo test -p git repository::tests::test_load_revisions --locked`
- `CARGO_INCREMENTAL=0 cargo test -p project
git_store::tests::test_decode_git_text_windows_1251_one_line_change
--locked`
- `CARGO_INCREMENTAL=0 cargo test -p project --test integration
test_restaging_hunk_after_optimistic_unstage --locked`
- `CARGO_INCREMENTAL=0 cargo check -p project --tests --locked`
- `CARGO_INCREMENTAL=0 cargo check -p git_ui --tests --locked`
- `cargo fmt --all --check`
- `git diff --check`
## Suggested .rules additions
- N/A
Release Notes:
- Fixed Git panel and Project Diff rendering for legacy-encoded text
files whose Git blobs are not valid UTF-8.
---------
Co-authored-by: Cole Miller <cole@zed.dev>
Fixes https://github.com/zed-industries/zed/issues/35780
Collab schema migration PR:
https://github.com/zed-industries/cloud/pull/3422
The corresponding database schema migration has been created in the
Cloud repo and applied to the production database.
Before, Zed scanned each and every entry in the tree down from the
directory it was opened in, except gitignored files and scan exclusions.
The approach is unchanged, if Zed detects it was open inside a git
repository: e.g. the directory open in Zed contains `.git` directory.
For the rest of the projects, 2 optimizations are made:
* Limit the depth of file scan traversal.
Now, `file_scan_depth` (default `5`) restricts Zed from traversing any
directory that has same number or more segments in its file path.
Such directories behave similar to gitignored directories: their
contents is not available in file finder, project search and project
panel, but can be lazily traversed when the directory is expanded (e.g.
project panel expands it or a nested file is open by path via terminal,
etc.)
To indicate that to the users, a status entry is shown firs time the
limitation is hit in the project:
<img width="858" height="133" alt="image"
src="https://github.com/user-attachments/assets/7da6cfbb-98b4-4cc3-bf2a-8902a9597a15"
/>
* During the scan, any git repositories that are not direct children of
the directory open in Zed (depth >= 2), are traversed and indexed
normally, but their git metadata is never fetched eagerly.
Only when Zed opens a buffer from that repo the git metadata is fetched
and applied.
All that combined now uses a way more moderate amount of CPU and RAM
when opening `~`:
<img width="1717" height="368" alt="Screenshot 2026-08-13 at 17 43 53"
src="https://github.com/user-attachments/assets/ec83e2a9-f7cc-452b-8eb7-af158284ca4e"
/>
File scan inclusions and exclusions are considered still for such
projects.
Set `file_scan_depth` to `0` to enable old behavior.
The setting is supported in the project settings, so custom values can
be set based on the project's structure.
---
Release Notes:
- Fixed Zed using a lot of memory and CPU in large, non-git-tracked,
directory trees
# Objective
I noticed that in workspace symbol search, the function's symbol kind
has become `Trait`.
## Solution
Add `to_proto` and a macro to define the mapping instead of `as i32`.
## Testing
Updated the test.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed `SymbolKind` mapping to LSP protocol values
### Summary
This PR fixes three small bugs that could cause a multi-workspace's
reported location to be out of sync with its actual location, which
causes following to break in multiplayer collaboration sessions.
The first bug was that each child workspace's title bar within a
multi-workspace had its own `cx.observe_window_activation` subscription
that would set the associated child workspace's project location. This
caused problems because it ran for all child workspaces instead of just
the active workspace within the multi-workspace. The fix was moving
`cx.observe_window_activation` to the `cx.observe_new` call in the
`call` crate that tracks newly created windows/multi-workspaces.
The second bug was caused by an incorrect `if` statement in a
`MultiWorkspace` subscription that would return early if the window was
active and the event emitted by `MultiWorkspace` wasn't
`ActiveWorkspaceChanged`. This caused issues when the window was
inactive because it would incorrectly set the active call's location to
the wrong project. The fix was making the early return happen if the
window wasn't active.
The third bug was that every child `Workspace` also observed window
activation and called `update_active_view_for_followers`. This allowed
hidden workspaces to report their active view instead of only the active
workspace.
### Testing
I added a property test for bugs one/two and another prop test for bug 3
Release Notes:
- collab: Fix out of sync following bugs
This adds a dedicated `AvailableLanguages` struct in preparation for a
more cabable language matching based on a given language config. No
functional changes, just shuffling some code around for this round.
Also made the LanguageMatcher non-cloneable in favor of wrapping it in
an Arc, since cloning is rather expensive for this and having a
reference is sufficient in all cases right now.
Release Notes:
- N/A
---------
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
extends the precedent set by #43703 (thanks @reflectronic) so that
`textDocument/inlayHint`, `textDocument/codeLens`, and
`textDocument/documentSymbol` correctly track multiple dynamic
registrations by id. previously each register call overwrote the slot on
`ServerCapabilities`, and unregister either silently did nothing
(inlayHint, documentSymbol — no match arm) or wiped the entire
capability (codeLens). now each method keeps its own id-keyed map on
`DynamicRegistrations`; the field on `ServerCapabilities` is cleared
only when the last registration is removed.
textDocument-sync notifications (`didChange`/`didSave`/`willSave`) are
deliberately out of scope — their fan-out semantics are ambiguous in the
spec and were flagged on @smitbarmase's earlier exploration in #36876.
verified with `cargo check -p project -p lsp -p editor`, `cargo test -p
project --test integration -- multi_registration` (3/3 passing), and
`cargo fmt -p project`.
closes part of #37838.
Release Notes:
- Improved support for language servers that dynamically register inlay
hints, code lens, or document symbols multiple times.
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
## Summary
Fixes the git commit template not loading in remote (SSH) projects. The
`load_commit_template_text` method in `git_store.rs` was a no-op for
`RepositoryState::Remote`, always returning `Ok(None)`. This patch adds
a `LoadCommitTemplate` RPC so the client can ask the remote host to read
its `commit.template` git config and return the file contents —
mirroring the existing `GetBlobContent` pattern.
## Changes
- **`crates/proto/proto/git.proto`** — new `LoadCommitTemplate` /
`LoadCommitTemplateResponse` messages.
- **`crates/proto/proto/zed.proto`** — registered envelope IDs 449/450.
- **`crates/proto/src/proto.rs`** — wired up message priority, request
pairing, and entity-message routing.
- **`crates/project/src/git_store.rs`** — added
`handle_load_commit_template` on the host side; replaced the `Ok(None)`
no-op on the remote side with the RPC call.
## Self-Review Checklist
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments — *no unsafe code
added*
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
— *no UI changes*
- [ ] Tests cover the new/changed behavior — *see "Testing notes" below*
- [x] Performance impact has been considered and is acceptable — *one
extra RPC on commit panel open for remote projects only; payload is a
single optional string*
## Testing notes — why no automated test
I did write an integration test (`test_remote_git_commit_template` in
`collab/tests/integration/git_tests.rs`, modeled after
`test_remote_git_head_sha`) along with the supporting changes to
`FakeGitRepositoryState` (adding a `commit_template` field +
`set_commit_template_for_repo` setter on `FakeFs`, since the fake
hardcoded `load_commit_template` to `None`).
The test compiled and the smaller crates (`fs`, `proto`, `project`)
checked clean, but `cargo test -p collab --test collab_tests`
cold-compile takes a very long time on my machine and I wasn't able to
confirm the test actually passed locally. Rather than push a test I
hadn't seen pass, I removed it. Happy to add it back in a follow-up PR
(or in this one if reviewers prefer) once I can run the collab suite
end-to-end — the diff is small and I can share it on request.
Verification was done end-to-end manually using a Docker dev container
as the SSH remote:
#### Closes#55265
Video :
[Screencast from 2026-05-02
18-09-03.webm](https://github.com/user-attachments/assets/9cb7f375-57fa-4af3-bde4-871c28f61efc)
Release Notes:
- Added support for loading git commit template messages in both remote
and collab projects.
---------
Co-authored-by: dino <dinojoaocosta@gmail.com>
Release Notes:
- Fixed remote support for adding paths to .gitignore
The problem solved by this PR is: when opening a remote project,
right-clicking a new file in the git panel and clicking "Add to
.gitignore" does not work.
---------
Co-authored-by: Cole Miller <cole@zed.dev>
# Objective
- Show accurate diff stats for each staged and unstaged projection of a
partially staged file in the Git panel.
- This was originally considered for
https://github.com/zed-industries/zed/pull/59884, but was scoped out of
that already-large PR and is being submitted separately as discussed
there.
## Solution
- Collect HEAD-to-index and index-to-worktree diff stats alongside the
existing combined HEAD-to-worktree stats.
- Carry the staged and unstaged stats through repository status
snapshots and remote status serialization.
- Use the stat matching the projected Git panel section while preserving
the combined stat for the other grouping modes.
- Update the fake Git repository and add regression coverage with
deliberately different staged and unstaged counts.
## Testing
- `cargo check -p git_ui`
- `cargo check -p collab`
- `cargo test -p git_ui
test_group_by_staging_section_membership_and_order --lib`
- `cargo test -p project --lib --no-run`
- `cargo fmt --all -- --check`
- `git diff --check`
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Fixed diff stats for partially staged files in the Git panel
# Objective
Add remote (SSH) and collaboration support for trashing and restoring
files in the project panel, which in turn enables undo/redo of trash
operations against remote and collab projects.
Relates to #5039.
## Solution
- Updated the project panel undo system to carry `TrashId` instead of
`TrashedEntry`.
- Using `TrashedEntry` could get hairy, as it includes paths, which
wouldn't play too nicely when using, for exapmle, macOS as the client
and Windows as the host. Using a simple identifier is much easier in
this regard and simplifies implementation.
- Enabled the Trash action and context-menu entry on remote projects,
and removed the command palette filter in `ProjectPanel::new` that was
still hiding the action on remote.
- As far as I can tell, there isn't a reliable way to detect whether a
given remote actually supports the OS trash, so we expose the action
everywhere rather than guessing. On a remote without trash support the
action will fail when invoked but this is a conscious tradeoff until we
find a better way to handle this.
- `fs` now tracks trashed files in a `SlotMap<TrashId, TrashedEntry>` on
each `Fs` implementation. Trashed files are referenced by an opaque
`TrashId` instead of passing a `TrashedEntry` around, which avoids
serializing filesystem paths in remote messages.
- Split the old `delete_entry(trash: bool)` API into distinct
`trash_entry`/`trash_file` (returning a `TrashId`) and
`delete_entry`/`delete_file` across `Project`, `Worktree`,
`LocalWorktree` and `RemoteWorktree`.
- This lets us drop the optional trash result (`Option<TrashedEntry>`)
from the delete path and require a `TrashId` from the trash path.
- Added new proto messages (`TrashProjectEntry`,
`TrashProjectEntryResponse`, `RestoreProjectEntry`,
`RestoreProjectEntryResponse`) to let clients request the host to trash
or restore entries.
- This deprecates `DeleteProjectEntry::use_trash`, but the host still
honors it. An older collab peer may request trashing via that flag
instead of the newer `TrashProjectEntry`. If the host ignored it, a
newer host would permanently delete a file the user meant to send to the
trash. The field will be removed in a later PR once all supported peers
use `TrashProjectEntry`.
## Testing
The following tests were introduced to ensure the new behavior is
correctly tested:
* `remote_server::remote_editing_tests::test_remote_trash_restore` –
Tests trashing a project entry in remote
*
`remote_server::remote_editing_tests::test_remote_delete_project_entry_with_trash`
– Test to ensure we continue respecting `DeleteProjectEntry::use_trash`
until it is fully removed
* `project_panel::tests::undo::trash_directory_undo_redo` – Not related
to these changes but a nice to have as we were missing a test ensuring
that trashing and then undoing and redoing it for a directory works as
expected
Besides these, the following scenarios were manually tested against a
remote session on the same machine (macOS):
- Trashing → Undo (Restore) → Redo (Trashing)
- Batch Trashing → Undo (Batch Restore) → Redo (Batch Trashing)
- Rename → Undo (Rename) → Redo (Rename)
- Move → Undo (Move) → Redo (Move)
- Batch Move → Undo (Batch Move) → Redo (Batch Move)
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable
Release Notes:
- N/A
---------
Co-authored-by: Yara <git@yara.blue>
This is necessary to remove some `util` dependencies from crates, as
well as better sharing for our projects. This also includes the WIP
AbsPath abstraction as well as some bug fixes from internal tooling.
Release Notes:
- N/A or Added/Fixed/Improved ...
Closes EP-193
Edit prediction context collection previously reused the editor
goto-definition / goto-type-definition path, which resolves every LSP
result into a `LocationLink` — opening a buffer per target (worktree
creation, buffer registration, anchor conversion) before edit prediction
gets a chance to filter. On remote projects the host additionally
created a peer-visible buffer per link and the client waited on each
one.
This adds a bespoke request path that returns raw results first and only
does the expensive work for results that survive filtering:
- New `EditPredictionDefinition { path: ProjectPath, range:
Range<Unclipped<PointUtf16>> }` boundary type. LSP wire types never
leave `lsp_command.rs`; the workspace-only filter and URI→`ProjectPath`
resolution happen while normalizing the LSP response, before any buffer
exists, so the type encodes the workspace-only invariant.
- New `GetEditPredictionDefinitions` /
`GetEditPredictionTypeDefinitions` LSP commands and proto messages.
Responses carry only path + UTF-16 range — the host never calls
`create_buffer_for_peer` and the client never waits on remote buffers.
- One public API: `Project::edit_prediction_definitions(buffer,
position, include_type_definitions, cx)` fires both LSP requests
concurrently and returns a merged, deduped list ("not capable" = empty).
The definition/type-definition split was never used downstream, so
`CacheEntry` now holds a single list and the fetch pipeline runs one
task per identifier.
- `edit_prediction_context` dedupes raw results before opening buffers;
survivors open via `project.open_buffer(ProjectPath)` (skipping the
invisible-worktree/yarn machinery of `open_local_buffer_via_lsp`, and
working identically on remote projects), then clip → anchor →
`MAX_TARGET_LEN`.
- Removes the `workspace_only` flag from `GetDefinitions` /
`GetTypeDefinitions`: edit prediction was its only user. The editor path
always sent `false`, which proto3 doesn't encode, so normal
goto-definition requests are wire-identical; old peers still sending
`true` get unfiltered results (graceful degradation). The proto field
numbers are `reserved`.
Tested with a local test proving filtering precedes buffer opening (an
out-of-workspace target never spawns a worktree, an oversized target is
excluded from related files) and a collab test proving the remote
round-trip returns correct paths/ranges without opening buffers on the
client.
Release Notes:
- N/A
Updates Collab to use the new `/internal/channel_members/fuzzy_search`
endpoint that searches users by username (instead of GitHub login).
Request and response shapes are the same.
Release Notes:
- N/A
Fetch tag names for commits included in blame data and pass them through
the blame rendering path. Render tag names as chips in the blame hover
tooltip and expanded blame popover. Tag lookup is best-effort, so blame
still renders if tag lookup fails.
> Note: Tag names are currently only fetched for local repositories. For
remote projects (SSH) and collab sessions, blame data travels over the
`BlameBufferResponse` proto message, which doesn't carry tag names yet.
## Testing
- `cargo test -p git
test_parse_tag_names_for_lightweight_and_annotated_tags`
- `cargo check -p git_ui`
- `cargo check -p editor`
- `cargo check -p project`
Manual testing:
- Opened `git: blame` on a file with a tagged commit.
- Verified tag chips are shown in the blame hover tooltip and expanded
popover.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and [icon]
(https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
Before:
<img width="1655" height="660" alt="before"
src="https://github.com/user-attachments/assets/dbefd729-f8df-48a9-8249-83c93518fe76"
/>
After:
<img width="1655" height="660" alt="after"
src="https://github.com/user-attachments/assets/2c1097de-b268-45d9-9d13-0f3e06058603"
/>
---
Release Notes:
- Git: Made tags visible in Git blame tooltips.
---------
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This passes the `include_remote_name` flag through the remote
`GetDefaultBranch` RPC instead of dropping it at the client/host
boundary. That lets remote repositories resolve default branches as
`origin/main` when needed, so worktree creation uses a valid remote
branch name instead of falling back to `main`.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes#59121
Release Notes:
- Fixed remote worktree creation from the default branch when the
default branch requires its remote name.
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This PR adds the new `id_v2` and `legacy_user_id` fields to the
`AuthenticatedUser` type and starts using them in place of the `id`
field.
Release Notes:
- N/A
LiveKit Cloud rejects room-join tokens as revoked when their `nbf`
predates a participant revocation. Zed generated those LiveKit JWTs with
`nbf: 0`, so a fresh participant token minted after stale connection
cleanup could still appear older than the cleanup and leave a user
joined at the collab layer without audio or screen sharing. This sets
`nbf` to the issuance time for room-join tokens while leaving admin/API
tokens unchanged, and adds regression coverage at the token, mock
LiveKit, and channel rejoin layers.
Closes FR-83
Release Notes:
- Fixed calls getting stuck without audio or screen sharing after
restarting Zed and rejoining a channel.
Summary
- Track whether a worktree root is itself a linked Git worktree.
- Use that metadata when computing project group keys so bare checkout
worktrees group under the repository identity path.
- Propagate the metadata through remote worktree protocols and add
local/remote regression coverage.
Background
Bare checkout layouts can place linked worktrees under the repository
identity directory, e.g. `/monty/.bare` with worktrees like
`/monty/feature-a`. We were treating those linked worktree paths as
separate project identities, which caused the sidebar to move agent
threads under the active worktree instead of the shared repository
group.
We also exclude adding this to collab intentionally, we can open a
different PR for that if we need to.
Closes#59910
Closes AI-431
Test Plan
- `cargo fmt --package project --package worktree --package
remote_server --package workspace --package collab --package proto`
- `git --no-pager diff --check`
- `cargo test -p project test_project_group_key -- --nocapture`
- `cargo test -p remote_server test_remote_root_repo_common_dir --
--nocapture`
- `cargo test -p worktree remote_worktree -- --nocapture`
- `cargo test -p workspace
test_remote_project_root_dir_changes_update_groups -- --nocapture`
- `cargo check -p collab`
Self-Review Checklist:
I've reviewed my own diff for quality, security, and reliability
Unsafe blocks (if any) have justifying comments
The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Tests cover the new/changed behavior
Performance impact has been considered and is acceptable
Release Notes:
- Fixed agent thread/sidebar grouping for Git worktrees backed by bare
checkouts.
---------
Co-authored-by: Anthony Eid <anthony@zed.dev>
# Objective
Prevent the unexpected modification of files which do not have
established formatting conventions.
Enabling format-on-save by default can cause changes in ecosystems that
don't have established official formatting conventions, or when the
formatting conventions came later through new tools (like JavaScript).
fixes#59427
## Solution
This PR inverts the default, choosing to disable format-on-save by
default, and allowing it to be enabled by users at a global or language
level for any language which has an official formatter. However, any
ecosystem which has an official formatter has been left enabled.
This means languages like Rust, Go, and Zig have `format_on_save`
enabled because they come with official formatting tools, while
JavaScript, C/C++, and Markdown have it disabled by default.
For existing users, this means their custom configurations will stay,
but any values that were previously "default" will be updated.
## Testing
- open a file for the language being tested (ex: `python`)
- formatting should only be enabled if the default for the *language* is
enabled
- formatting can be enabled or disabled at the language level or the
global level in the settings
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
---
Release Notes:
- Disable format-on-save by default, except for languages with official
formatters
This PR unships shared threads.
The feature is not used much and is getting in the way, at this point.
These are only ever made available to staff.
The corresponding database schema migration has been created in the
Cloud repo and applied to the production database.
Release Notes:
- N/A
- **Revert "livekit: Preserve tokens on channel rejoin (#59388)"**
- **Revert "call: Log LiveKit connection info refresh outcomes in retry
loop (#59205)"**
- **Revert "audio: Fix phantom presence in channels (#59195)"**
- **proto: Reserve RejoinRoomResponse field 4 after revert**
We've observed a spike of phantom collaborator issues after this fixes.
This sucks and I'm quite unhappy about it.
# Objective
- Describe the objective or issue this PR addresses.
- If you're fixing a specific issue, use "Fixes #X" for each issue as
[described in the GitHub
docs](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).
## Solution
- Describe the solution used to achieve the objective above.
## Testing
- Did you test these changes? If so, how?
- Are there any parts that need more testing?
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
## Self-Review Checklist:
- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable
## Showcase
> This section is optional. If this PR does not include a visual change
or does not add a new user-facing feature, you can delete this section.
- Help others understand the result of this PR by showcasing your
awesome work!
- If this PR includes a visual change, consider adding a screenshot,
GIF, or video
- A before/after comparison is very useful for changes to existing
features!
While a showcase should aim to be brief and digestible, you can use a
toggleable section to save space on longer showcases:
<details>
<summary>Click to view showcase</summary>
My super cool demos here
</details>
---
Release Notes:
- N/A
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This fixes archive cleanup for agent threads so Zed only removes
worktrees it explicitly created, rather than treating every linked
worktree under the configured managed directory as safe to delete. When
Zed creates a worktree, it now records it in the local database along
with the creation time of the worktree's git metadata directory
(`.git/worktrees/<name>/`). Archive planning requires that record, and
right before deleting anything, Zed re-stats the directory and compares
creation times: if the worktree was removed and recreated outside Zed
(or the time can't be read at all), deletion is skipped and the stale
record is dropped. Every failure mode fails safe by leaving the
directory untouched.
For remote (SSH) projects, the stat runs on the remote host via a new
`GitWorktreeCreatedAt` request. Worktrees created by a different Zed
install (another release channel, or another machine connecting to the
same host) have no record in the local database and are therefore never
auto-archived, which is the intended conservative behavior.
Closes https://github.com/zed-industries/zed/issues/57155
Closes AI-352
Release Notes:
- Fixed archiving an agent thread incorrectly deleting manually-created
git worktrees.
- **collab: Don't revoke LiveKit token when rejoining the same channel**
- **call: Bound LiveKit reconnect attempts and decouple from collab
rejoin**
# Objective
- Describe the objective or issue this PR addresses.
- If you're fixing a specific issue, use "Fixes #X" for each issue as
[described in the GitHub
docs](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).
## Solution
- Describe the solution used to achieve the objective above.
## Testing
- Did you test these changes? If so, how?
- Are there any parts that need more testing?
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
## Self-Review Checklist:
- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable
## Showcase
> This section is optional. If this PR does not include a visual change
or does not add a new user-facing feature, you can delete this section.
- Help others understand the result of this PR by showcasing your
awesome work!
- If this PR includes a visual change, consider adding a screenshot,
GIF, or video
- A before/after comparison is very useful for changes to existing
features!
While a showcase should aim to be brief and digestible, you can use a
toggleable section to save space on longer showcases:
<details>
<summary>Click to view showcase</summary>
My super cool demos here
</details>
---
Release Notes:
- N/A or Added/Fixed/Improved ...
# Objective
You know that feeling? You join the channel, crack a banger joke to
break the ice - yet, nobody laughs. Worse yet, you're swarmed with Slack
messages like "Piotr, we can't hear you". After a while you realize that
maybe it's for the best as the joke was not as good as you've initially
thought, but still: it's possible to get into a state where other
participants see you as muted. You don't hear anybody. Bummer.
That's what happened when we were hoping to pair with @dinocosta today.
I could not hear him. However, he had some logs to share which indicated
that while collab thought he's in the room with me, live-kit was
rejecting his token as it was revoked.
What clanker helped us figure out is that there's a race-y code path
where if you disconnect from a given room abruptly, collab can put you
in a limbo.
<details><summary>Clanker's idea of what's going on</summary>
<p>
Found the whole story. Here's what those logs mean and where the bug
surface is.
What's happening
The error at room.rs:1809 is the detach_and_log_err on the task spawned
by spawn_room_connection (crates/call/src/call_impl/room.rs:1754-1810).
So this is the initial livekit::Room::connect call failing — the LiveKit
SDK internally retried 3 times (that's the retrying... (1/3) lines), got
401 invalid token: revoked every time, gave up, and the error propagated
out of the ? at line 1762 and was logged-and-dropped.
The "revoked" comes from the collab server: leave_room_for_session
(crates/collab/src/rpc.rs:4091-4095) calls
live_kit.remove_participant(livekit_room,
session.user_id().to_string()), and LiveKit Cloud treats a removed
participant's token as revoked. Crucially, the LiveKit identity is just
the user id, not the connection id, while the DB-side
leave_room(connection_id) is per-connection. leave_room_for_session
fires from three places:
1. Explicit leave (rpc.rs:1666)
2. connection_lost, after RECONNECT_TIMEOUT expires (rpc.rs:1342)
3. Stale-connection cleanup when re-joining a channel (rpc.rs:3401)
So there's a race: an old connection's delayed cleanup (path 2) can fire
after the same user has rejoined the room on a new connection and been
issued a fresh token — and remove_participant(room, user_id)
kicks/revokes the user's current LiveKit participant, because the
identity is shared across connections. Quick restart of Zed, two
instances, or a reconnect that straddles the server's RECONNECT_TIMEOUT
all set this up.
Client-side consequences (this connects to your first question)
When that initial connect fails:
- this.live_kit stays None and this.diagnostics is never created — lines
1780-1791 only run on success. So in this failure mode CallDiagnostics
doesn't exist at all; the "stats never overridden" path I described
earlier isn't even reached.
- There is no retry and no token refresh. The failure is
detach_and_log_err'd and that's the end of it. The user stays in the
room at the collab level (status is still Online, they appear in the
channel), but with no audio and no way to recover short of leaving and
rejoining.
- Even a collab-level reconnect doesn't help: RejoinRoomResponse
(crates/proto/proto/call.proto:58) carries no live_kit_connection_info,
and rejoin() never calls spawn_room_connection — the LiveKit connection
is established exactly once, in Room::new, with the token from the
original join response.
So the user-visible symptom is: you're "in" the call, everyone sees you,
but you can't hear or be heard, and there's no error surfaced in the UI
— just this log line.
If you want to fix it, the two angles are server-side (make
leave_room_for_session skip the LiveKit removal when the user still has
a live connection in that room, or scope LiveKit identities
per-connection) and client-side (treat a failed spawn_room_connection as
fatal-but-recoverable: surface it, and have rejoin request fresh
connection info instead of silently carrying on). Happy to dig into
either if you want.
</p>
</details>
So tl;dr: our token was swapped out underneath us. One way to "fix" it
was to just go into another channel, but that was a bummer.
We have a good and reliable repro for it though: `kill -9 $ZED_PID`
followed by an attempt to rejoin the same channel within 30s would
consistently put us in that state.
Put another way: you panic and thus do not send a clean "leave room"
message to LK. if you rejoin the channel within 30s (after restarting),
LK will invalidate the very token you're attempting to use (as it cleans
up old tokens).
It may also happen without crashing, but that's the most reliable way to
repro the issue. Long story short your LK token gets tainted and you
can't share the audio anyhow.
## Solution
The fix is both client and server-side.
On client's side, we now retry the reconnection with a back-off and we
grab a fresh token off of reconnect attempts.
On server's side, we share the current token on room reconnect attempts.
We've also tweaked the call diagnostics to not use `unwrap_or_default`
so much: it was hard for us to tell that we're in a totally bogus state
and we've only reached that conclusion based on source code analysis. We
now show some generic "ok you're in a borked state pls report a bug"
message instead.
## Testing
We added tests that present the flacky scenario. We've also tried to
setup an infra to test the new behaviour, but sadly, local LK instance
seems to revoke tokens more leniently than the prod..
OTOH, we are quite confident that once new collab is deployed, we'll be
able to mince the new tokens and life is going to be good. Even when
running against the current prod instance we could see that our code
changed the behaviour for the better, as now we'll actually try to use a
new token if one is provided by collab. For that to happen we need to
redeploy though.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Fixed a race condition that caused collab users to not receive/send
any audio to their peers.
---------
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Dino <dino@zed.dev>
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
This PR changes how base texts are managed by the `buffer_diff` crate,
to enable keeping two diff entities alive that share the same base text
buffer entity. Previously, each diff owned its own base text buffer and
edited it when calling `BufferDiff::set_snapshot`, so the only way to
reuse the same base text between two diffs was to have two independent
buffers for it, which is pretty inefficient.
After this PR, each diff still has a base text buffer, but
`set_snapshot` doesn't edit it. Instead, that responsibility moves into
the caller. For updating the base text buffer, this PR also introduces a
new pair of APIs, `Buffer::snapshot_with_edits` and
`Buffer::fast_forward`, which allow us to move the parsing of the new
base text into the background and then install the new syntax tree
synchronously on the foreground.
The git store uses the low-level APIs `set_snapshot` and `fast_forward`
directly, and manages the head text and index text buffers itself
(garbage-collecting them when they're no longer needed); this enables
adding an `open_staged_diff` API which returns a diff between the
managed index buffer and the managed head buffer (the latter is also
used for the uncommitted diff's base text). Other downstreams don't need
to reuse a base text buffer, and those have been migrated to use the
high-level `set_base_text` API, which now calls `set_snapshot` and
`fast_forward` internally, with a guard to prevent concurrent updates.
Another change worthy of note is that we now always diff the old base
text with the new base text to create `snapshot_with_edits`.
There are also some incidental bug fixes:
- Fixed an issue where a dangling weak unstaged diff could stick around
in the git store forever
- Restored the `IndexMatchesHead` optimization that had become
inoperative in the remote case
- Fixed a crash in the multibuffer due to the handling of
`BufferDiffEvent::LanguageChanged`, which could cause the multibuffer to
have transforms that were inconsistent with the diff base text.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes ZED-81P
Release Notes:
- Fixed a rare crash that could occur while using the uncommitted diff.
---------
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>