# 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
# Objective
- This PR fixes#63760. According to git docs "Patterns read from
$GIT_DIR/info/exclude".
## Solution
- We were reading the directory's absolute path instead of reading the
absolute path to the repository's common .git directory.
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] 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
---
Release Notes:
- Fixed "Add to .git/info/exclude" not working when used from a
secondary git worktree
---------
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
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
Closes https://github.com/zed-industries/zed/issues/64118https://github.com/user-attachments/assets/f2152411-6011-4d87-8c8e-d83bed0afbbc
`ty` returns completion text without a `textEdit` or a completion-list
default edit range:
```json
{"label":"value2=","insertText":"value2="}
```
The [LSP
specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem)
explicitly states:
> The `insertText` is subject to interpretation by the client side.
For the reported example, `ˇ` marks the cursor:
```py
test(value1, ˇvalue2)
```
Previously, Zed inferred only a range covering the existing `value2`.
Without an insert range, completion acceptance bypassed
`completions.lsp_insert_mode` and replaced that argument with `value2=`,
producing:
```py
test(value1, value2=)
```
The patch also infers an insert range, ending at the cursor rather than
the end of the word.
This lets the existing setting choose between inserting before `value2`
and replacing it.
When a prefix has already been typed, insertion replaces that prefix
while preserving the text after the cursor.
With the unchanged default, `"replace_suffix"`, Zed replaces text after
the cursor only when it is a suffix of the completion’s label.
Here, `value2` is not a suffix of `value2=`, so Zed inserts and
produces:
```py
test(value1, value2=value2)
```
The suffix and subsequence checks now use the raw LSP `label`, not
decorated menu text or `filterText`.
These checks select the edit range; `insertText` still supplies the
inserted text in this example.
Release Notes:
- Fixed insertion modes for completions without edit ranges
# Objective
Fix an issue in SSH remote mode where reopening a newly created file at
a path that was previously renamed could incorrectly reuse the buffer
for the renamed file.
## Solution
When handling UpdateBufferFile, remove the buffer's previous path from
path_to_buffer_id when the file path changes.
## Testing
Tested manually on Linux using SSH remote mode.
Reproduction before the fix:
touch A
Open A from the project panel
mv A B
touch A
Open the new A from the project panel
Before the fix, opening A reused the buffer for B.
After the fix:
B continues to use the original buffer.
The recreated A opens as a separate buffer.
Renaming an opened remote file still updates the existing buffer to the
new path correctly.
## 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 an issue in SSH remote mode where reopening a newly created file
at a path that was previously renamed could incorrectly reuse the buffer
for the renamed file.
---------
Signed-off-by: xiaoff <xiaoff0019@outlook.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
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>
Closes https://github.com/zed-industries/zed/discussions/51582
During testing, spotted that if I toggle off the hints with the modifier
keys, they are resurrected on scrolling — fixed that in a small commit
on top.
Release Notes:
- Supported commands in inlay hints
# 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.
# Objective
Fixes#63869 — "Copy Value" in the debugger panel copied the trimmed
display preview instead of the full value.
## Solution
Evaluate the variable's `evaluateName` via the DAP `evaluate` request
using the `clipboard` context (falling back to `variables`), and copy
the returned result. Falls back to the display value if evaluation
fails.
## Testing
- `cargo test -p debugger_ui --lib` passes.
---
Release Notes:
- Fixed debugger "Copy Value" copying the truncated preview instead of
the full value.
This is a pure refactoring PR to make our FS watching code more
testable. It does two things:
1. Make the `GlobalWatcher` be owned by `RealFs` or `FakeFs` instead of
being a process-wide global.
2. Delete `FakeWatcher` and its `Watcher` implementation, and instead
mock the notify crate itself in `FakeFs` tests via the existing
`WatchBackend` trait. Now all our FS watching code down to the notify
boundary runs the same in tests and production.
Release Notes:
- N/A
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>
Closes https://github.com/zed-industries/zed/issues/62391
Workspace diagnostics were polled incorrectly:
* Workspace pulls were cancelled and re-sent every 120s.
Pyright-family servers hold the request open forever and re-analyze the
whole workspace on every new one, so the retry loop leaked memory; the
request is now kept open (long-poll), as the spec recommends.
* No re-trigger after a server closed a workspace pull.
ty closes the request when diagnostics change and expects the client to
re-pull
* `ServerCancelled` / `retriggerRequest` was ignored.
The spec defaults cancellation data to `{ retriggerRequest: true }`, so
these errors now retrigger workspace pulls and retry document pulls
instead of failing them.
* LSP error codes were erased with `anyhow!(message)`
Errors now carry a typed `lsp::ResponseError` through the chain,
replacing four copies of `ends_with(...)` matching against
rust-analyzer's literal message texts.
* Workspace partial results were not tied to their request.
Stale or numeric-token `$/progress` reports were applied as current data
and could poison `previousResultIds` until a server restart; reports are
now matched against the exact in-flight token.
* `interFileDependencies` was never read.
Every edit re-pulled all other open buffers even for servers declaring
no inter-file dependencies (e.g. ruff); cross-buffer pulls are now gated
on that capability.
* Cross-buffer pulls were unpaced.
All open buffers were pulled back-to-back after each edit debounce; they
are now paced at one document per 500ms.
* The failure valve killed workspace pulls permanently.
51 hard errors over a server's lifetime silently stopped its workspace
pulls until restart
* Concurrent refresh waiters could be dropped instantly.
The capacity-1 refresh channel resolved a second concurrent waiter to
`false` immediately, and request errors dropped queued refreshes;
waiters now queue and carry over to the retried pull.
Release Notes:
- Fixed basedpyright memory leak caused by incorrect workspace
diagnostics polling
# Objective
Diagnostics published by a language server survive the buffer being
detached from that server, so a file keeps showing errors from a server
that is no longer reading it.
To reproduce:
1. Install the [GitHub Actions
extension](https://zed.dev/extensions/github-actions).
2. Open a composite action definition,
`.github/actions/<name>/action.yml`.
3. Select the GitHub Actions language for it in the language selector.
That server validates the file as a workflow, so it reports `on` and
`jobs` as missing and `inputs`, `outputs` and `runs` as unexpected.
4. Select YAML again, since GitHub Actions was not what you wanted here.
5. The errors are still there. `editor: restart language servers` does
not clear them either. They go away only once the buffer is dropped, in
practice by switching to another project and back.
Two things keep them alive:
- `LocalLspStore::unregister_buffer_from_language_servers` drops the
snapshot bookkeeping and sends `didClose`, but never clears what that
server already published. `LspStore::set_language_for_buffer` calls it,
so a language change detaches the buffer while leaving the old verdict
on it.
- Restarting does not reach the server either.
`stop_local_language_servers_for_buffers` collects servers through
`language_server_ids_for_buffer`, evaluated against the language the
buffer has *now*. The old server no longer matches, so it is never
stopped, and it is typically still running legitimately for other files
in the project. Only `stop_local_language_server` clears diagnostics,
and it is never reached for that server.
## Solution
The first commit cleared the detached server's diagnostics inside
`unregister_buffer_from_language_servers`, on `didClose`. Review pointed
out that publishes are applied by path regardless of registration, so a
running server can legitimately describe a file that is no longer open
in it, and that this clearing also left `diagnostic_summaries` and the
worktree's per-path diagnostics stale.
The second commit reworks this. Detaching a buffer now only drops the
buffer's entry in `buffers_opened_in_servers` and the path's
pull-diagnostics result ids for the servers it left, two pieces of
per-server state that previously leaked. After the buffer is registered
with the servers of its new language, the server tree is rebuilt through
`refresh_server_tree`, the same pruning that runs on settings changes:
every server no registered buffer maps to any more is stopped through
`stop_local_language_server`, whose path already clears buffer
diagnostic sets, `diagnostic_summaries` and worktree diagnostics. A
detached server that other buffers still map to keeps running and keeps
everything it published. An earlier revision judged orphanhood by
`buffers_opened_in_servers`, which is only filled in once a server runs,
so it stopped servers that were still starting;
`test_context_completion_provider_mentions` caught that.
## Testing
Added `test_selecting_a_language_clears_the_old_servers_diagnostics`: a
Rust buffer gets a diagnostic from a fake server, the buffer is switched
to JavaScript, and afterwards both the buffer's diagnostics and the
project-level `diagnostic_summary` must be empty. It fails on `main`,
where the message survives the switch:
```text
< ["unused constant"]
> []
```
The test passes with the rework and fails without it. The `project` test
suite, `script/clippy` and style checks ran green on the reworked change
in CI. Tested on Linux; the change is 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
## Showcase
The buffer is back on YAML, as the status bar shows, while the workflow
schema's complaints from the GitHub Actions server are still on screen:
`on` and `jobs` reported missing, and `inputs`, `outputs` and `runs`
reported as unexpected values.
<img width="2514" height="2008" alt="Screenshot From 2026-08-30
21-52-12"
src="https://github.com/user-attachments/assets/040898a6-cc61-41b6-88c6-f346666e8e13"
/>
---
Release Notes:
- Fixed diagnostics from the previous language server staying on a file
after changing its language.
---------
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
- Render PDF pages on-demand per active page instead of stitching entire document into one giant texture (fixing 22k px Vulkan texture atlas overflow and driver crash).
- Add toolbar navigation buttons (previous, next, page indicator).
- Add keyboard shortcuts (PageUp/PageDown, Left/Right arrows, j/k, Home/End) for page navigation.
- Add status bar metadata for active PDF page index and total page count.
# Objective
Keep Git-specific diff-base knowledge in `GitStore`, where Git
operations are defined.
Related to FR-135.
## Solution
Give each `BufferDiff` its supported hunk operations. Keep editor hooks
limited to rendering custom controls.
## 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
Closes https://github.com/zed-industries/zed/issues/59689
Symbolicating the reporter's profile (21s of typing, Windows) showed
`Window::draw` taking 63% of the main thread at ~62 redraws/s (p95 draw
= 40ms), and 30.9% of the *entire* main thread inside
`EditorconfigStore::properties()`:
every `LanguageSettings` lookup — several per frame via
`DisplayMap::tab_size`, `MultiBuffer::language_settings`, indent guides,
and `LspStore::supports_range_formatting` — re-ran the full editorconfig
resolution (ancestor walk plus `ec4rs::Section::apply_to` over every key
of every section) and then cloned the whole `LanguageSettings` struct.
Real-world `.editorconfig` files carry hundreds of irrelevant keys
(`dotnet_*`, `ij_*`, ...), so the per-call map assembly dominated:
`RtlAllocateHeap` 7.3%, `memcmp` 7.0%, `memcpy` 6.6%,
`Properties::find_idx` 5.9% self time.
## Commit 1: resolve only the editorconfig properties Zed reads
- `EditorconfigStore::properties()` now applies only the 7 keys Zed
consumes (`indent_style`, `indent_size`, `tab_width`, `end_of_line`,
`max_line_length`, `insert_final_newline`, `trim_trailing_whitespace`)
instead of materializing every key of every matching section.
- Worktrees with no editorconfig at all now return `None` early instead
of allocating an empty `Properties` and forcing a `LanguageSettings`
clone on every lookup.
- Adds an `editor_render_with_editorconfig` benchmark: maximized window,
file-backed buffer at `src/app/components/editor_pane.ts`, a root
`.editorconfig` shaped after [the second
report](https://github.com/zed-industries/zed/issues/59689#issuecomment-5475124891)
(no `root = true`, a `[*]` section with ~3200 keys, 25 sections with
large `{...}` alternation lists) plus a 4-level nested chain, and a
typing workload (insert/delete + cursor moves).
```
main (f034409b6a): time: [37.905 ms 38.005 ms 38.107 ms]
time: [38.069 ms 38.157 ms 38.248 ms]
this commit: time: [2.8547 ms 2.8618 ms 2.8702 ms]
time: [2.8372 ms 2.8435 ms 2.8510 ms]
```
The candidate is ~13x faster.
GPUI frame report over the same workload: window draw p50 12.71 ms →
0.76 ms, p99 23.38 ms → 1.22 ms; frame budget overruns 556 → 0.
## Commit 2: cache resolved `LanguageSettings` per buffer
- `Buffer` and `BufferSnapshot` now hold `resolved_settings:
Arc<LanguageSettings>`, recomputed by a `SettingsStore` global observer.
- Global methods for retrieving the settings are still there, but buffer
only uses them for multi buffer's `language_settings_at` with an
injected language different from the buffer's primary; cold scopes such
as extension API, LSP workspace configuration, server startup still use
the global resolution
Same benchmark, same method, on top of commit 1 (alternated runs, quiet
machine):
```
commit 1: time: [2.8547 ms 2.8618 ms 2.8702 ms]
time: [2.8372 ms 2.8435 ms 2.8510 ms]
this commit: time: [1.6256 ms 1.6325 ms 1.6403 ms]
change: [-42.685% -42.270% -41.854%] (p = 0.00 < 0.05)
time: [1.6221 ms 1.6284 ms 1.6352 ms]
```
Window draw p50 0.76 ms → 0.47 ms, p99 1.22 ms → 0.69 ms, max 1.02 ms;
zero frame-budget overruns on both sides.
Combined, the benchmark's per-keystroke cost drops ~23x vs `main`
(37.9–38.2 ms → 1.62–1.64 ms).
Release Notes:
- Improved typing and rendering performance in projects that use
`.editorconfig` files.
Closes#22473.
Some language servers report a syntax error whose range starts at the
end
of one line and ends at the start of the next — e.g.
Pyright/ruff/ty/pyrefly
on a Python function missing its trailing `:`:
```
range: { start: { line: 0, character: 11 }, end: { line: 1, character: 0 } }
```
That range spans nothing but the line terminator, so there's no glyph
left
for the editor to underline. The diagnostic is real (it shows up in the
status bar and the diagnostics panel) but never gets a visible squiggle
in
the buffer, which makes it very easy to miss — especially since a
missing
`:` is one of the most common Python typos.
## Fix
`update_buffer_diagnostics` already collapses *empty* ranges onto an
adjacent character. This adds a sibling case for ranges that are exactly
`line_start..next_line_start` (i.e. cover only the terminator): they're
pulled back onto the last character of the line they actually concern,
the same way empty ranges are. Ranges are only touched when the end
position is precisely the start of the next line, so genuinely
multi-line
diagnostics that happen to *start* at an end-of-line position are left
untouched.
## Testing
Added `test_diagnostic_range_spanning_line_terminator` in
`project_tests.rs`, modeled on the neighboring
`test_empty_diagnostic_ranges`,
asserting the closing `)` of `def main()` gets the error highlight
instead
of the range being invisible.
Verified against real language servers: reproduced the exact same
`start..next_line_start` range on ruff 0.16.4, ty 0.0.74, and pyrefly
1.2.0
for a missing `:`, matching the LSP trace pyright produced in #22473.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Release Notes:
- Fixed diagnostics not being underlined when their range covers only a
line terminator (e.g. a missing `:` in Python)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Previously, we'd panic when constructing multibuffer excerpts in
response to "find all references" if a language server returned a
reversed range. This PR makes it so we normalize those ranges using the
existing `range_from_lsp` helper before passing them to
`set_excerpts_for_path`. It also replaces several other places where we
were calling `point_from_lsp` on each endpoint of a range separately
with `range_from_lsp`.
Closes ZED-79W
Co-authored-by: Mikayla <mikayla@zed.dev>
Release Notes:
- Fixed a crash with some languages servers when handling
`editor::FindAllReferences`.
# Objective
Closes#4868.
Reintroduce automatic language detection for user-created untitled
buffers without allowing it to override language choices made by users
or owning surfaces.
More details:
+ https://github.com/zed-industries/zed/pull/61201
+ https://github.com/zed-industries/zed/pull/61351
## Solution
VS Code distinguishes between inferred and explicitly selected
languages, and only continues automatic detection in the former state.
This change introduces a buffer-local opt-in flag with the same purpose.
It defaults to disabled and is enabled only for user-created untitled
buffers and restored untitled buffers that did not persist a language.
Choosing a language through the language picker disables detection,
while automatically detected languages remain eligible for later
redetection. Buffers whose language is assigned by their owning surface,
such as Channel Notes, never opt in.
Rather than adding persisted state or changing the database schema,
restoration reuses the existing optional serialized language. An
opted-in buffer that remains Plain Text is serialized without a language
and resumes detection after restoration. Once automatic detection has
selected a language, that language is serialized normally and
restoration leaves detection disabled, effectively treating it as an
explicit selection. This also matches VS Code's effective default after
restoration.
Candidate selection adopts VS Code's confidence-group filtering. Results
below the minimum confidence are discarded, and candidate groups are
accepted only after a clear confidence gap. The detector then selects
the first available Zed language without eagerly loading every
candidate. I then added a separate switching rule: Zed keeps the current
language unless another candidate has a clear confidence advantage,
reducing language changes as model scores fluctuate during editing.
Model inference runs off-thread on a bounded sample, and stale results
are discarded.
I also evaluated penalizing YAML and using stricter global switching
thresholds, but neither reliably resolves the ambiguity between Markdown
and YAML. In the reproduction from #61351, Betlang assigns YAML more
than 80% confidence during editing and peaks around 90%. This is
reasonable from the content alone: `#` denotes a YAML comment, while `-`
denotes a sequence item. Adding ordinary prose or a fenced code block
shifts the model strongly toward Markdown. A fixed penalty would overfit
this example and risk suppressing legitimate YAML detection. Since
known-language buffers do not opt in and manual language selection takes
precedence, I consider this level of ambiguity acceptable and prefer not
to introduce language-specific score tuning.
Model inference itself is fast. As noted in #61201:
> In release builds on Linux with an Intel Core i5-13600KF, the
`betlang::detect` call alone typically completes within 3 ms when
processing the maximum sampled input.
The main concern is the work surrounding inference. Without debouncing,
rapid typing creates a snapshot and launches a background detection task
on every keystroke, while the detected language may change before the
user has finished typing. This change therefore adds a 200 ms debounce
delay, allowing detection to run and update the language only after the
user pauses.
## Testing
Automated coverage verifies that short inputs remain Plain Text,
sufficient Rust content is detected as Rust, replacing it with Go
content triggers redetection, and manually selecting Plain Text prevents
subsequent automatic changes. Restoration coverage verifies that a
stored language keeps detection disabled, while an empty language-less
untitled buffer resumes detection.
Beyond the automated tests, I manually tested automatic language
detection while editing untitled buffers.
## 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/103fc731-e12e-48d6-ae89-03978f1c9d74
---
Release Notes:
- Added automatic language detection for untitled buffers.
---------
Co-authored-by: Max Stevens <maxstevens2708@gmail.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Follow-up https://github.com/zed-industries/zed/pull/55153
Fixes
https://github.com/zed-industries/zed/discussions/54553#discussioncomment-17091833
When a repository stores its Git data in `~/work/zed/.bare`, creating a
linked worktree from Zed is broken in two visible ways:
- Zed creates `random-code` at:
```
~/work/worktrees/.bare/random-code/.bare
```
instead of:
```
~/work/worktrees/zed/random-code/zed
```
- Because the checkout ends in `.bare`, Zed displays `.bare` as its
worktree name in the title bar, worktree picker, and agent thread
sidebar. Every worktree Zed creates from that repository gets the same
`.bare` label instead of its generated name.
This problem is specific to the path Zed creates. Worktrees using either
of these layouts are not malformed:
```
~/work/worktrees/zed/random-code/zed
~/work/zed/some-worktree
```
https://github.com/zed-industries/zed/pull/55153 introduced repository
identities for bare repositories. This PR uses that identity in the
remaining worktree naming and creation paths.
There are three repository layouts to account for:
- In a normal repository, the common Git directory is `~/work/zed/.git`,
so Zed derives the main worktree at `~/work/zed`. Existing behavior
remains unchanged.
- With `~/work/zed/.bare`, Git has no main worktree. A checkout named
main is still a linked worktree. Zed now treats `~/work/zed` as the
repository identity, creates new worktrees under `~/work/worktrees/zed`,
and displays their generated names.
- With a bare repository named `~/work/foo.git`, `foo.git` is the
repository identity. Its worktree path remains
`~/work/worktrees/foo.git/random-code/foo.git`. Zed uses foo.git to
derive the worktree name random-code, while displaying the repository
itself as foo.
Existing malformed worktrees such as
`~/work/worktrees/.bare/random-code/.bare` are not moved and may
continue to display `.bare`.
Broken:
<p>
<img width="238" height="58" alt="image"
src="https://github.com/user-attachments/assets/1504e4d0-c34a-471f-8e76-c5ec062b1d77"
/>
</p>
<p>
<img width="274" height="49" alt="image"
src="https://github.com/user-attachments/assets/13266ca9-4e64-426c-b03c-e23c924a3597"
/>
</p>
<p>
<img width="574" height="85" alt="image"
src="https://github.com/user-attachments/assets/0fca58c7-5be7-4d36-9067-066d0b80caf8"
/>
</p>
Release Notes:
- Fixed linked worktree names and creation paths for bare Git
repositories.
[## Context
Fixes a bug where the Bookmarks tab, opened via `editor: view
bookmarks`, showed a stale snapshot and had to be reopened to display
bookmarks added or removed while it was open.
Closes#56268
`view_bookmarks` built a one-time multibuffer and never subscribed to
`BookmarkStore`. The store also calls `cx.notify()` while resolving
serialized bookmarks into anchors, even though that bookkeeping does not
represent a user-visible bookmark change. Observing all notifications
would therefore feed resolution notifications back into the refresh
mechanism and cause redundant cancellation and requeue work. The store
now emits a dedicated `BookmarkStoreEvent::BookmarksChanged` from
bookmark mutation paths, while `resolve_all` deliberately does not emit
it.
Refresh state lives on a `BookmarksView` entity that holds the tab's
multibuffer. Split editors already share that multibuffer, so they share
the view as well. Each store event drives one reconciliation regardless
of how many split editors display it. The view remains alive as long as
any split retains it, so closing the original tab does not stop the
remaining split from refreshing.
Each refresh reconciles excerpts per path instead of rebuilding the
multibuffer: `set_excerpts_for_path` updates files that still contain
bookmarks, while `remove_excerpts` removes files whose final bookmark
was deleted. This follows the per-path reconciliation pattern used by
project diff views.
`view_bookmarks` creates the tab synchronously and populates it
asynchronously, following the existing patterns in
`ProjectDiagnosticsEditor::deploy` and
`ProjectSearchView::existing_or_new_search`. Repeated invocations focus
the existing Bookmarks tab instead of opening duplicates.
Video of the manual test:
[Screencast from 2026-05-11
02-03-38.webm](https://github.com/user-attachments/assets/8ac97245-09ff-4a79-9634-20cb8dfbb7eb)
## How to Review
**`crates/project/src/bookmark_store.rs`**
Adds `BookmarkStoreEvent::BookmarksChanged`, emitted from bookmark
mutation paths such as toggle, label edit, file rename or deletion,
clear, and deserialization, but not from anchor resolution. It also adds
`BookmarkStore::is_empty`.
**`crates/editor/src/bookmarks.rs`**
Adds `BookmarksView`, which holds the multibuffer, store subscription,
and in-flight refresh task. It reconciles excerpts and emits
`BookmarksViewEvent::Populated(ranges)` for per-editor highlights and
cursor placement. It also caches the latest populated ranges and replays
them when an editor attaches, allowing a split created after population
to display bookmark highlights immediately.
**`crates/editor/src/editor.rs`**
Adds the shared `bookmark_view`, a per-editor subscription to its
population events, and `bookmark_initial_selection_pending` for initial
cursor placement. `Editor::clone` attaches the split to the shared view
instead of creating another store subscription and refresh task.
**`crates/editor/src/editor_tests.rs`**
Adds five tests covering automatic addition and removal across multiple
files, removal of a file's path after its final bookmark is deleted,
initial cursor placement, a bookmark change racing initial population,
splitting while population is pending, cached highlights in a
late-attaching split, and continued refreshing after the original tab
releases its view handle.
## Self-Review Checklist
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Fixed Bookmarks tab not updating when bookmarks are added or removed
while the tab is open](https://github.com/zed-industries/zed/pull/56363)
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
## Objective
Fixes an issue where language servers provided by extensions are not
included in the settings schemas served by remote servers. When
connecting to a remote server via the
`open_remote_project_with_existing_connection` flow (dev servers,
multi-workspace reopen, git worktree picker), setup-N servers never
received any extension sync, so their schemas omitted extension-provided
language servers, breaking language support for remote development.
## Solution
Three changes ensure that language servers provided by extensions are
included in the settings schemas served by remote servers:
1. **Register connections opened via
`open_remote_project_with_existing_connection` with the extension
store**: dev servers, multi-workspace reopen, and the git worktree
picker now register their connections with the extension store so that
extensions are synced to those servers. Previously only the
`open_remote_project` flow did this, so setup-N servers never received
any extension sync and their schemas omitted extension language servers.
2. **Emit `ExtensionsInstalledChanged` from `HeadlessExtensionStore`
after sync and install complete**: this allows the remote process to
invalidate its cached settings schemas. The local `ExtensionStore`
already emitted this event; without it, the remote schema cache stayed
stale for the lifetime of the server process.
3. **Include available LSP adapters in the project settings schema**:
this matches the user settings schema (this was fixed for `settings` in
#46766 but never applied to `project_settings`).
## Testing
- Manually verified that after connecting to a remote server via the
`open_remote_project_with_existing_connection` flow (including dev
servers and the git worktree picker), extension language servers appear
in the schema served by the remote server.
- Verified that `ExtensionsInstalledChanged` is emitted after sync and
install complete, and that the remote schema cache is correctly
invalidated and rebuilt.
- Verified that the `project_settings` schema includes the available LSP
adapters, matching the behavior of the user settings schema.
- Reviewers should focus on the multi-workspace reopen and git worktree
picker flows (the two previously unregistered flows) to confirm that
extension language servers are correctly synced and appear in the
schema.
## Self-Review Checklist:
- [x] 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)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
**Server / Project** — before the fix
<img width="1061" height="749" alt="image"
src="https://github.com/user-attachments/assets/d6c5459f-58fb-4769-a701-f6fb317adc37"
/>
**Local**
<img width="1119" height="449" alt="image"
src="https://github.com/user-attachments/assets/fb30a1e2-74f0-4439-a25f-1022ed164074"
/>
---
Release Notes:
- Fixed an issue where language servers provided by extensions were
missing from settings schemas served by remote servers.
---------
Co-authored-by: vancez <vancez@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
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#62972
Completed Agent terminal cards remain alive so conversation history can
render their output. Each retained terminal also kept its Alacritty PTY
sender alive, which retained a poller and one kqueue descriptor after
the command exited.
Terminal lifecycle state is now more strongly typed to prevent this
class of regression. Task terminals are created through a sealed
`TerminalMode` API that pairs their running state with the correct
completion channel, while PTY resources use explicit active and released
states. This makes invalid lifecycle combinations unrepresentable
instead of relying on callers to coordinate independent `Option` values
correctly.
- Release the live PTY sender after final terminal output is captured.
- Preserve PTY process metadata and working-directory behavior for
historical terminal cards.
- Represent PTY resources as explicit active or released lifecycle
states.
- Construct interactive and task terminals through typed modes so a task
cannot omit its running state or use a mismatched completion channel.
- Keep resource release idempotent for later ACP release and entity
destruction.
- Add a macOS regression test that retains the historical terminal
entity while exercising the complete sandboxed terminal tool path.
Release Notes:
- Fixed Agent terminal tool calls leaking file descriptors on macOS.
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
# Objective
Fixes#28398
## Solution
This issue was occurring because `ProjectSearch` stored
`Entity<MultiBuffer>` with has reference counted pointers to underlying
buffers
The function `remove_deleted_buffers` is renamed to
`remove_stale_buffers` and it now also checks for buffers which do not
have a file associated with them and are closed. Then, it removes them
from `Entity<MultiBuffer>` stored in ProjectSearch. This function is
also called in again when workspace is upgraded.
## Testing
Bug being fixed was manually verified.
## 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
Release Notes:
- Fixed project search retaining results from closed untitled buffers.
---------
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
Closes https://github.com/zed-industries/zed/issues/9318
Most of the diff is tests.
Notable behavior changes during this mode, compared to current mode:
* unsaved changes in multi buffer won't spawn any "save or not" pop-ups
during typing — submitting the query still does it, as before.
Unsaved files will stay so after being sorted out on typing: we cannot
save them by default as file-less buffers need a save modal; Zed should
already be able to persist and restore such changes on restart.
* gutter may jump during search, if new excerpts with large line numbers
appear during query input — yet it would not shrink back to avoid
excessive flickering and rewraps
Release Notes:
- Enabled project search on type by default (use `{ "search": {
"search_on_type": false } }` to restore the previous behavior)