Merge origin/main into feat/copy-from-diff-view

#799 landed first and rebuilt the overlay as one virtualised `gpui::list`
of flat rows, deleting the card, the hunk nesting and the three row
builders this branch had hung the drag on. The selection is re-applied
onto that model rather than rebuilt beside it.

**Where a row is.** `DiffRow::Split`/`Unified` now carry a `RowAt` — the
file's path and the `RowId { hunk, row }` the range is keyed on. The
list index is deliberately *not* that key: `resync_list` splices only
the rows between a shared prefix and suffix, so collapsing a file above
the selection moves every index below it, and a range keyed on those
would go on naming the same slots while the code under them changed.
`RowAt::path` is a `SharedString` so one rides on every line of a patch
for a refcount bump.

**Where the selection is read.** `diff_rows_list` clones it into a
`Drag` once a frame and the list's item builder reads that. It stays out
of `RowsKey` on purpose: a selection changes what a row looks like, not
which rows there are, so a step of a drag notifies, re-renders the rows
on screen, and never re-flattens the patch — `gpui::list` calls its item
builder for every visible row on each layout pass, so the highlight is
current without the key knowing anything about it.

**Where the menu is.** `diff_file_card` was the old host and is gone.
Copy Selected Lines now hangs on the selected rows themselves. The
overlay root was the other candidate and is not usable: the header
already carries `document_header_menu`, and gpui-component's context
menu opens on a bubbling right-press without stopping propagation, so
two of them over one click would open two popups.

The padding-cell fix from 87dc26f0..f7fd1d43 is carried across: a blank
half of a split row takes the same press and move listeners as any other
cell, and the selection colour when the range covers it.

Also drops `HunkRows::len`/`is_empty`, which only the deleted card body
called.

Tests: the drag suite moves to `selection_gpui_tests` and drives the
same cases against the new state, including the upward drag; two new
`diff_list` tests pin the coordinates a row carries — that they name the
patch rather than the list, and that they survive a collapse above them.
This commit is contained in:
l0ng-ai
2026-09-07 22:40:07 +08:00
11 changed files with 2054 additions and 862 deletions
+39
View File
@@ -5,6 +5,43 @@ All notable changes to tty7 are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [26.9.1] - 2026-09-07
### Fixed
- **A remote server that will not start now says why** (#774). A remote
workspace could sit in a loop nobody could get out of: every reconnect failed
with "started but nothing was answering on the control socket after 15s", the
strip showed a copy bar frozen at 100%, and no button was offered. A daemon
whose control listener would not open logged one line and kept running — and a
running daemon holds the single-server lock, so every later start stood down
at once and every probe failed, forever. Whether something else is serving is
now settled by connecting rather than by reading the errno, and a daemon that
cannot listen exits. The reason is kept too: the remote daemon's output and
exit status land beside the binary, stamped with the launch's own nonce so a
restart never reads the outgoing daemon's status as the incoming one's, and a
start that has already failed no longer waits out the full timeout. On the
window side, an automatic reconnect retires its install progress instead of
drawing an install still in flight, and a long error no longer stretches the
status card past the window and takes the retry button off screen with it.
- **A stale pane socket no longer stops every later daemon** (#779). A daemon
that died without unlinking its Unix socket left a file `bind` refuses, so the
client launched a daemon, it exited on the bind, and the client launched
another — forever. The removal is now decided by the single-server seat rather
than the pidfile: holding the seat means nobody else can be serving that config
dir, so anything still at the endpoint belongs to a process that is gone.
Where there is no seat, a socket that answers is refused rather than removed.
### Changed
- **The control dialect is v9.** v8 added the project verbs; this build takes
them back out and speaks v7's messages again, message for message — but the
number does not go back with them. v8 is deployed, and a number that moves
backwards stops being an identity: a 7 on the wire would mean either "before
projects" or "after them" depending on which build put it there, and the
handshake has nothing but the number to tell the two apart.
## [26.9.0] - 2026-09-04
### Added
@@ -4401,6 +4438,8 @@ Initial release.
- zsh shell integration (OSC 7 cwd + OSC 133 prompt marks) via a throwaway `ZDOTDIR`.
- Native macOS light/dark themes that follow the system appearance.
[26.9.1]: https://github.com/l0ng-ai/tty7/compare/v26.9.0...v26.9.1
[26.9.0]: https://github.com/l0ng-ai/tty7/compare/v26.8.3...v26.9.0
[26.8.3]: https://github.com/l0ng-ai/tty7/compare/v26.8.2...v26.8.3
[26.8.2]: https://github.com/l0ng-ai/tty7/compare/v26.8.1...v26.8.2
[26.8.1]: https://github.com/l0ng-ai/tty7/compare/v26.8.0...v26.8.1
Generated
+107 -57
View File
@@ -298,7 +298,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -309,7 +309,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -376,6 +376,12 @@ dependencies = [
"password-hash",
]
[[package]]
name = "array-init"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
[[package]]
name = "arraydeque"
version = "0.5.1"
@@ -681,6 +687,20 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "async_zip"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb7f5f40e1eb30949a266fc900d37fd3267c7baf50c3705ac09c5d8ced5def63"
dependencies = [
"async-compression",
"binrw",
"crc32fast",
"futures-lite 2.6.1",
"pin-project",
"thiserror 2.0.20",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -891,7 +911,7 @@ dependencies = [
"bitflags 2.13.1",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"itertools 0.11.0",
"log",
"prettyplease",
"proc-macro2",
@@ -902,6 +922,30 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "binrw"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ad120d555272286c1017d25165ab8bd74806f13fc85b258484ec7e4ce75458f"
dependencies = [
"array-init",
"binrw_derive",
"bytemuck",
]
[[package]]
name = "binrw_derive"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df92e0e9baae4dc82c7bad7715ca40c0a5c71539057bf2ea04a5c29c980410b"
dependencies = [
"either",
"owo-colors",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -1483,7 +1527,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"gpui_util",
"indexmap",
@@ -2047,7 +2091,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"proc-macro2",
"quote",
@@ -2104,7 +2148,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -2142,7 +2186,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
"libloading 0.8.9",
"libloading 0.7.4",
]
[[package]]
@@ -2394,7 +2438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3199,7 +3243,7 @@ dependencies = [
"log",
"presser",
"thiserror 2.0.20",
"windows 0.62.2",
"windows 0.58.0",
]
[[package]]
@@ -3225,7 +3269,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"accesskit",
"anyhow",
@@ -3416,7 +3460,7 @@ dependencies = [
[[package]]
name = "gpui_linux"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"accesskit",
"accesskit_unix",
@@ -3467,7 +3511,7 @@ dependencies = [
[[package]]
name = "gpui_macos"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"accesskit",
"accesskit_macos",
@@ -3514,7 +3558,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -3525,7 +3569,7 @@ dependencies = [
[[package]]
name = "gpui_platform"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"console_error_panic_hook",
"gpui",
@@ -3538,7 +3582,7 @@ dependencies = [
[[package]]
name = "gpui_shared_string"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"schemars",
"serde",
@@ -3548,7 +3592,7 @@ dependencies = [
[[package]]
name = "gpui_util"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"log",
@@ -3557,7 +3601,7 @@ dependencies = [
[[package]]
name = "gpui_web"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -3581,7 +3625,7 @@ dependencies = [
[[package]]
name = "gpui_wgpu"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"bytemuck",
@@ -3610,7 +3654,7 @@ dependencies = [
[[package]]
name = "gpui_windows"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"accesskit",
"accesskit_windows",
@@ -3939,7 +3983,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"async-compression",
@@ -3964,7 +4008,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -4057,7 +4101,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.2",
"windows-core 0.58.0",
]
[[package]]
@@ -4581,9 +4625,9 @@ dependencies = [
[[package]]
name = "keyring"
version = "4.1.6"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72585bb6cc9bc370d1d545b7e23fcce71dfd4461c5e15275e3cf51bdfd9a980a"
checksum = "2270074a3d26bcac93c1dc5d2845eb4c089e8d761ccf6e0ea266a16004640627"
dependencies = [
"apple-native-keyring-store",
"keyring-core",
@@ -4901,9 +4945,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
dependencies = [
"serde_core",
"value-bag",
@@ -5092,7 +5136,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"bindgen",
@@ -5202,7 +5246,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -5258,7 +5302,7 @@ dependencies = [
"once_cell",
"png",
"thiserror 2.0.20",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -5481,7 +5525,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -6006,6 +6050,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "owo-colors"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
[[package]]
name = "p256"
version = "0.14.0-rc.15"
@@ -6204,7 +6254,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"collections",
"serde",
@@ -6900,7 +6950,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -7207,7 +7257,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"derive_refineable",
]
@@ -7250,7 +7300,7 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"bytes",
@@ -7611,7 +7661,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -7784,7 +7834,7 @@ dependencies = [
[[package]]
name = "scheduler"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"async-task",
"backtrace",
@@ -8389,7 +8439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -8515,7 +8565,7 @@ dependencies = [
"cfg-if",
"libc",
"psm",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -8627,7 +8677,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"heapless",
"log",
@@ -8917,7 +8967,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -9384,7 +9434,7 @@ dependencies = [
"once_cell",
"png",
"thiserror 2.0.20",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -9775,11 +9825,11 @@ dependencies = [
[[package]]
name = "tty7"
version = "26.9.0"
version = "26.9.1"
dependencies = [
"alacritty_terminal",
"anyhow",
"async_zip",
"async_zip 0.0.19",
"core-foundation 0.10.0",
"gpui",
"gpui-component",
@@ -9820,7 +9870,7 @@ dependencies = [
[[package]]
name = "tty7-cli"
version = "26.9.0"
version = "26.9.1"
dependencies = [
"alacritty_terminal",
"anyhow",
@@ -9835,7 +9885,7 @@ dependencies = [
[[package]]
name = "tty7-core"
version = "26.9.0"
version = "26.9.1"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -9869,7 +9919,7 @@ dependencies = [
[[package]]
name = "tty7-server"
version = "26.9.0"
version = "26.9.1"
dependencies = [
"libc",
"tempfile",
@@ -9896,7 +9946,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset 0.9.1",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -10108,11 +10158,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"async-fs",
"async_zip",
"async_zip 0.0.18",
"collections",
"command-fds",
"dirs",
@@ -10147,7 +10197,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"perf",
"quote",
@@ -10156,9 +10206,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.24.1"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -10740,7 +10790,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
@@ -11491,7 +11541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -12048,7 +12098,7 @@ dependencies = [
[[package]]
name = "zlog"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"anyhow",
"chrono",
@@ -12093,7 +12143,7 @@ dependencies = [
[[package]]
name = "ztracing"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
dependencies = [
"tracing",
"tracing-subscriber",
@@ -12104,7 +12154,7 @@ dependencies = [
[[package]]
name = "ztracing_macro"
version = "0.1.0"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#d99a40a5f79a173465feec00363d480f75881a81"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#ece710e365575bc08656c4a8d20488769b2de9a6"
[[package]]
name = "zune-core"
+11 -3
View File
@@ -172,7 +172,7 @@ windows-sys = { version = "0.61", features = [
# The standalone updater extracts only Windows release ZIPs. Reuse the async
# reader already present in Cargo.lock and enable only the Deflate codec emitted
# by PowerShell's Compress-Archive.
async_zip = { version = "0.0.18", default-features = false, features = ["deflate"], optional = true }
async_zip = { version = "0.0.19", default-features = false, features = ["deflate"], optional = true }
# COM for toast branding (`core::aumid`): IShellLinkW + IPropertyStore stamp
# System.AppUserModel.ID onto the Start Menu shortcut, which Windows requires
@@ -271,7 +271,7 @@ edition = "2024"
# they must never drift apart. This is the line a release bump edits (and the
# one nightly's `awk '/^version = /'` stamps: it is the first such line in the
# file, since the package entries above are all `version.workspace = true`).
version = "26.9.0"
version = "26.9.1"
[workspace.dependencies]
# Our fork's `tty7` branch carries the local customizations tty7 relies on:
@@ -367,7 +367,7 @@ lto = "thin"
codegen-units = 1
# ---- gpui fork ------------------------------------------------------------
# Our `tty7` branch (cut from the pinned upstream rev, three commits on top) carries:
# Our `tty7` branch (cut from the pinned upstream rev) carries:
#
# 1. `prefers_ime_for_printable_keys` takes the keystroke, so an input handler can
# answer per key instead of per view. tty7 needs it for Option-as-Meta — macOS
@@ -386,6 +386,14 @@ codegen-units = 1
# 3. resvg/usvg bumped 0.45 → 0.47 so gpui's SVG stack unifies with the resvg
# tty7 pins directly above (follow-up to the #227 dependabot bump).
#
# 4. `ListState::with_size_hint`. A list counts an item it has not laid out yet
# as zero tall, so a long document reports itself as about one viewport and
# a scrollbar reading that height drags one viewport and stops — the diff
# overlay scrolls a whole working tree through one of these. The hint counts
# the unmeasured items at an estimate instead, which `measure_all` would
# settle exactly at the price of laying out the whole document on the first
# frame (see `ui::diff_overlay::DIFF_LINE_H`).
#
# Patching by source rather than editing the `gpui`/`gpui_platform` pins above is
# deliberate: `gpui-component` declares its own `gpui` from the upstream URL, and
# a plain pin swap would put two incompatible copies of gpui in the tree. `[patch]`
+81 -4
View File
@@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize};
pub const MAX_FRAME: usize = 64 * 1024 * 1024;
/// A frame is a little-endian `u32` length, a one-byte kind, then the payload.
const HEADER: usize = 5;
pub const PROTOCOL_VERSION: u32 = 6;
pub const FEATURE_PANE_OWNER: &str = "pane-owner";
@@ -952,9 +955,33 @@ pub fn write_frame<W: Write>(w: &mut W, kind: u8, payload: &[u8]) -> io::Result<
"frame payload exceeds MAX_FRAME",
));
}
w.write_all(&(len as u32).to_le_bytes())?;
w.write_all(&[kind])?;
w.write_all(payload)?;
// One write, not three. The pane socket is a loopback `TcpStream` with
// `TCP_NODELAY` set, so three `write_all`s put the length, the kind and the
// payload on the wire as three separate segments, and the reader on the far
// side wakes from `read()` three times for one frame. Under a PTY flood the
// daemon frames every ConPTY read, so that is two extra syscalls on each
// side per frame, tens of thousands a second (issue #713).
let mut header = [0u8; HEADER];
header[..4].copy_from_slice(&(len as u32).to_le_bytes());
header[4] = kind;
let mut bufs = [io::IoSlice::new(&header), io::IoSlice::new(payload)];
let mut rest: &mut [io::IoSlice<'_>] = &mut bufs;
while !rest.is_empty() {
match w.write_vectored(rest) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"the frame could not be written in full",
));
}
// A writer that does not implement `write_vectored` natively falls
// back to writing the first non-empty slice, so this loop still
// terminates — it just costs the two writes it used to cost.
Ok(n) => io::IoSlice::advance_slices(&mut rest, n),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
@@ -984,7 +1011,6 @@ pub fn is_error_kind(kind: u8) -> bool {
}
pub fn take_frame(buf: &mut Vec<u8>) -> io::Result<Option<(u8, Vec<u8>)>> {
const HEADER: usize = 5;
if buf.len() < HEADER {
return Ok(None);
}
@@ -2046,6 +2072,57 @@ mod tests {
assert!(buf.is_empty());
}
/// The pane socket has `TCP_NODELAY` set, so a write is a segment and a
/// segment is a wakeup on the far side. A frame must therefore cost one
/// write, not one for the length, one for the kind and one for the payload
/// (issue #713) — at flood rates that difference is tens of thousands of
/// syscalls a second on each end.
#[test]
fn a_frame_is_one_write_on_a_vectored_writer() {
#[derive(Default)]
struct Counting {
writes: usize,
bytes: Vec<u8>,
}
impl Write for Counting {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writes += 1;
self.bytes.extend_from_slice(buf);
Ok(buf.len())
}
fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
self.writes += 1;
let mut n = 0;
for b in bufs {
self.bytes.extend_from_slice(b);
n += b.len();
}
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let mut w = Counting::default();
write_frame(&mut w, kind::OUTPUT, b"a chunk of pty output").expect("write the frame");
assert_eq!(w.writes, 1, "one frame must cost one write");
// An empty payload is a frame too — the header still has to land, and
// the empty second slice must not spin the loop.
let mut empty = Counting::default();
write_frame(&mut empty, kind::DETACH, &[]).expect("write the empty frame");
assert_eq!(empty.writes, 1);
// Whatever the write count, the bytes on the wire are unchanged: a
// `read_frame` over them gives back exactly what went in.
let mut cursor = io::Cursor::new(w.bytes);
assert_eq!(
read_frame(&mut cursor).expect("read it back"),
(kind::OUTPUT, b"a chunk of pty output".to_vec())
);
}
#[test]
fn from_frame_rejects_unknown_kind() {
assert!(ClientMsg::from_frame(99, vec![]).is_err());
+575
View File
@@ -0,0 +1,575 @@
//! Flattening a diff snapshot into the one-dimensional list of rows the
//! overlay scrolls.
//!
//! The overlay used to build its whole patch as a nested element tree —
//! a card per file, a hunk header and one element per line inside it, every
//! one of them constructed on every frame. A `20_000` line budget is around
//! `120_000` elements to lay out, and gpui notifies the view on each scroll
//! wheel event, so a diff of any size rebuilt the entire tree tens of times a
//! second.
//!
//! [`gpui::list`] only builds the rows it can see, but it is one-dimensional:
//! it takes an index, not a tree. So the tree is flattened here, once per
//! change to what is on screen.
use std::collections::HashMap;
use gpui::SharedString;
use crate::core::config::DiffViewMode;
use crate::terminal::git_diff::{
AUTO_COLLAPSE_LINES, DiffSnapshot, FileDiff, FileStatus, MAX_RENDERED_FILES, Truncation,
};
use crate::ui::diff_rows::{RowId, SplitRow, UnifiedRow, split_hunk, unified_rows};
/// Everything the file header row draws, lifted out of its [`FileDiff`].
///
/// A copy rather than a borrow: the row list outlives the frame that built it,
/// and these are a handful of small fields against a file's whole patch.
#[derive(PartialEq, Eq)]
pub(crate) struct FileHead {
/// Position in `snap.files`, for the element id and nothing else.
pub(crate) index: usize,
pub(crate) path: String,
/// `old → new` for a rename, the path itself otherwise.
pub(crate) shown_path: String,
pub(crate) status: FileStatus,
pub(crate) added: u32,
pub(crate) removed: u32,
pub(crate) binary: bool,
pub(crate) expandable: bool,
pub(crate) expanded: bool,
}
/// Where a drawn line sits in the patch: which file's rows it belongs to, and
/// its place among them.
///
/// Carried on the row rather than read off its position in the list. The list
/// is spliced — collapsing a file above this one moves every index below it —
/// so a range keyed on list positions would go on naming the same slots while
/// the code under them changed.
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct RowAt {
/// One of these rides on every line of a patch, so the path is shared
/// rather than copied per row.
pub(crate) path: SharedString,
pub(crate) id: RowId,
}
#[derive(PartialEq, Eq)]
pub(crate) enum DiffRow {
/// The space that used to be the `gap_3` of a flex column.
Gap,
/// The banner above an auto-collapsed tree. Its text is derived from the
/// snapshot at render time, so nothing is carried here.
Oversized,
FileHeader(FileHead),
HunkHeader {
text: String,
/// The first hunk of a file follows its header and needs no rule
/// above it; every later one is separating itself from the lines of
/// the hunk before.
leads: bool,
},
Split {
row: SplitRow,
at: RowAt,
},
Unified {
row: UnifiedRow,
at: RowAt,
},
Truncated(Truncation),
MoreFiles {
rest: usize,
},
UntrackedHeader {
total: usize,
},
Untracked {
index: usize,
path: String,
},
MoreUntracked {
rest: usize,
},
}
/// The rows of the whole overlay, in the order they scroll past.
pub(crate) fn build_rows(
snap: &DiffSnapshot,
expanded: &HashMap<String, bool>,
focused: Option<usize>,
mode: DiffViewMode,
oversized: bool,
) -> Vec<DiffRow> {
let mut out: Vec<DiffRow> = Vec::new();
// Stands in for the gap between the groups this list used to be a flex
// column of: gpui's list stacks its items with nothing between them.
let gap = |out: &mut Vec<DiffRow>| {
if !out.is_empty() {
out.push(DiffRow::Gap);
}
};
if oversized {
out.push(DiffRow::Oversized);
}
let shown = snap.files.len().min(MAX_RENDERED_FILES);
for (idx, file) in snap.files.iter().enumerate() {
if focused.is_some_and(|f| f != idx) {
continue;
}
if focused.is_none() && idx >= shown {
break;
}
let is_expanded = if focused == Some(idx) {
expanded.get(&file.path).copied().unwrap_or(true)
} else {
file_expanded(file, expanded, oversized)
};
gap(&mut out);
out.extend(file_rows(idx, file, is_expanded, mode));
}
if focused.is_none() && snap.files.len() > shown {
gap(&mut out);
out.push(DiffRow::MoreFiles {
rest: snap.files.len() - shown,
});
}
if focused.is_none() && !snap.untracked.is_empty() {
gap(&mut out);
out.extend(untracked_rows(snap));
}
out
}
/// The single synthesized file an untracked file's preview shows.
///
/// `usize::MAX` keeps its element ids clear of the real list's.
pub(crate) fn preview_rows(file: &FileDiff, mode: DiffViewMode) -> Vec<DiffRow> {
file_rows(usize::MAX, file, true, mode)
}
fn file_rows(index: usize, file: &FileDiff, expanded: bool, mode: DiffViewMode) -> Vec<DiffRow> {
let expandable =
!file.binary && (!file.hunks.is_empty() || file.truncated == Some(Truncation::Budget));
let shown_path = match &file.old_path {
Some(old) => format!("{old}{}", file.path),
None => file.path.clone(),
};
let mut rows = vec![DiffRow::FileHeader(FileHead {
index,
path: file.path.clone(),
shown_path,
status: file.status,
added: file.added,
removed: file.removed,
binary: file.binary,
expandable,
expanded,
})];
if expanded && (!file.hunks.is_empty() || file.truncated.is_some()) {
let path = SharedString::from(file.path.clone());
for (h, hunk) in file.hunks.iter().enumerate() {
rows.push(DiffRow::HunkHeader {
text: hunk.header.clone(),
leads: h == 0,
});
let at = |row| RowAt {
path: path.clone(),
id: RowId { hunk: h, row },
};
match mode {
DiffViewMode::Split => rows.extend(
split_hunk(&hunk.lines)
.into_iter()
.enumerate()
.map(|(r, row)| DiffRow::Split { row, at: at(r) }),
),
DiffViewMode::Unified => rows.extend(
unified_rows(&hunk.lines)
.into_iter()
.enumerate()
.map(|(r, row)| DiffRow::Unified { row, at: at(r) }),
),
}
}
if let Some(reason) = file.truncated {
rows.push(DiffRow::Truncated(reason));
}
}
rows
}
fn untracked_rows(snap: &DiffSnapshot) -> Vec<DiffRow> {
let total = snap.untracked_count();
let shown = &snap.untracked[..snap.untracked.len().min(MAX_RENDERED_FILES)];
let mut rows = vec![DiffRow::UntrackedHeader { total }];
for (index, path) in shown.iter().enumerate() {
rows.push(DiffRow::Untracked {
index,
path: path.clone(),
});
}
if total > shown.len() {
rows.push(DiffRow::MoreUntracked {
rest: total - shown.len(),
});
}
rows
}
/// Whether a file's lines show on their own, absent an explicit choice.
pub(crate) fn file_expanded(
file: &FileDiff,
expanded: &HashMap<String, bool>,
collapse_all: bool,
) -> bool {
if let Some(&want) = expanded.get(&file.path) {
return want;
}
!collapse_all && file.added + file.removed <= AUTO_COLLAPSE_LINES
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::git_diff::{DiffLine, Hunk, LineKind};
fn line(kind: LineKind, old: Option<u32>, new: Option<u32>) -> DiffLine {
DiffLine {
kind,
old_no: old,
new_no: new,
text: "x".to_string(),
}
}
/// One removal answered by one addition, wrapped in a context line either
/// side — four lines unified, three rows split.
fn hunk() -> Hunk {
Hunk {
header: "@@ -1,3 +1,3 @@".to_string(),
lines: vec![
line(LineKind::Context, Some(1), Some(1)),
line(LineKind::Removed, Some(2), None),
line(LineKind::Added, None, Some(2)),
line(LineKind::Context, Some(3), Some(3)),
],
}
}
fn file(path: &str, hunks: usize) -> FileDiff {
FileDiff {
path: path.to_string(),
old_path: None,
status: FileStatus::Modified,
added: 1,
removed: 1,
binary: false,
truncated: None,
hunks: std::iter::repeat_n(hunk(), hunks).collect(),
}
}
fn snapshot(files: Vec<FileDiff>) -> DiffSnapshot {
DiffSnapshot {
files,
..Default::default()
}
}
/// `(path, hunk, row)` for each line row, in list order.
fn line_coords(rows: &[DiffRow]) -> Vec<(String, usize, usize)> {
rows.iter()
.filter_map(|row| match row {
DiffRow::Split { at, .. } | DiffRow::Unified { at, .. } => {
Some((at.path.to_string(), at.id.hunk, at.id.row))
}
_ => None,
})
.collect()
}
/// A row says where it is in the *patch*, not where it is in the list.
/// Collapsing a file splices the rows below it up the list; a range keyed
/// on list positions would stay where it was and come away naming other
/// code.
#[test]
fn a_line_row_names_its_file_and_its_place_in_the_hunk() {
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
let open = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
let coords = line_coords(&open);
assert_eq!(
coords,
vec![
("a.rs".to_string(), 0, 0),
("a.rs".to_string(), 0, 1),
("a.rs".to_string(), 0, 2),
("a.rs".to_string(), 0, 3),
("b.rs".to_string(), 0, 0),
("b.rs".to_string(), 0, 1),
("b.rs".to_string(), 0, 2),
("b.rs".to_string(), 0, 3),
]
);
let collapsed = HashMap::from([("a.rs".to_string(), false)]);
let after = build_rows(&snap, &collapsed, None, DiffViewMode::Unified, false);
assert_eq!(
line_coords(&after),
coords[4..].to_vec(),
"b.rs's lines moved up the list and kept the coordinates they had"
);
}
/// Every hunk restarts the row count, so a range that spans two of them is
/// read hunk by hunk rather than as one run.
#[test]
fn each_hunk_numbers_its_own_rows() {
let snap = snapshot(vec![file("a.rs", 2)]);
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Split, false);
let hunks: Vec<_> = line_coords(&rows)
.into_iter()
.map(|(_, hunk, row)| (hunk, row))
.collect();
assert_eq!(hunks, vec![(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]);
}
fn shape(rows: &[DiffRow]) -> Vec<&'static str> {
rows.iter()
.map(|row| match row {
DiffRow::Gap => "gap",
DiffRow::Oversized => "oversized",
DiffRow::FileHeader(_) => "file",
DiffRow::HunkHeader { .. } => "hunk",
DiffRow::Split { .. } => "split",
DiffRow::Unified { .. } => "unified",
DiffRow::Truncated(_) => "truncated",
DiffRow::MoreFiles { .. } => "more-files",
DiffRow::UntrackedHeader { .. } => "untracked-header",
DiffRow::Untracked { .. } => "untracked",
DiffRow::MoreUntracked { .. } => "more-untracked",
})
.collect()
}
fn open(paths: [&str; 1]) -> HashMap<String, bool> {
paths.into_iter().map(|p| (p.to_string(), true)).collect()
}
#[test]
fn an_open_file_flattens_to_a_header_a_hunk_header_and_a_row_per_line() {
let snap = snapshot(vec![file("a.rs", 1)]);
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
assert_eq!(
shape(&rows),
["file", "hunk", "unified", "unified", "unified", "unified"]
);
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Split, false);
assert_eq!(
shape(&rows),
["file", "hunk", "split", "split", "split"],
"the split view pairs the removal with the addition that replaced it"
);
}
#[test]
fn only_the_first_hunk_of_a_file_follows_its_header() {
let rows = build_rows(
&snapshot(vec![file("a.rs", 2)]),
&HashMap::new(),
None,
DiffViewMode::Unified,
false,
);
let leads: Vec<bool> = rows
.iter()
.filter_map(|r| match r {
DiffRow::HunkHeader { leads, .. } => Some(*leads),
_ => None,
})
.collect();
assert_eq!(
leads,
[true, false],
"the second hunk draws the rule that separates it from the first"
);
}
#[test]
fn a_collapsed_file_is_a_single_row() {
let snap = snapshot(vec![file("a.rs", 1)]);
let shut: HashMap<String, bool> = [("a.rs".to_string(), false)].into_iter().collect();
let rows = build_rows(&snap, &shut, None, DiffViewMode::Unified, false);
assert_eq!(shape(&rows), ["file"]);
}
#[test]
fn a_truncation_note_lands_under_the_lines_it_is_about() {
let mut f = file("a.rs", 1);
f.truncated = Some(Truncation::Budget);
let rows = build_rows(
&snapshot(vec![f]),
&HashMap::new(),
None,
DiffViewMode::Unified,
false,
);
assert_eq!(*shape(&rows).last().unwrap(), "truncated");
}
#[test]
fn files_are_separated_by_a_gap_and_the_list_never_opens_with_one() {
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
let shut: HashMap<String, bool> =
snap.files.iter().map(|f| (f.path.clone(), false)).collect();
let rows = build_rows(&snap, &shut, None, DiffViewMode::Unified, false);
assert_eq!(shape(&rows), ["file", "gap", "file"]);
}
#[test]
fn the_oversized_banner_leads_and_collapses_what_was_not_asked_for() {
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
let rows = build_rows(&snap, &open(["b.rs"]), None, DiffViewMode::Unified, true);
assert_eq!(
shape(&rows),
[
"oversized",
"gap",
"file",
"gap",
"file",
"hunk",
"unified",
"unified",
"unified",
"unified"
],
"the file the reader opened stays open; the other one does not"
);
}
#[test]
fn a_focused_file_is_the_only_one_flattened() {
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
let rows = build_rows(
&snap,
&HashMap::new(),
Some(1),
DiffViewMode::Unified,
false,
);
assert_eq!(
shape(&rows),
["file", "hunk", "unified", "unified", "unified", "unified"]
);
let DiffRow::FileHeader(head) = &rows[0] else {
panic!("the focused file leads");
};
assert_eq!(head.path, "b.rs");
assert_eq!(head.index, 1, "the element id still points into `files`");
}
#[test]
fn the_file_list_is_capped_and_the_tail_gets_one_line() {
let snap = snapshot(
(0..MAX_RENDERED_FILES + 25)
.map(|i| {
let mut f = file(&format!("f{i}.rs"), 1);
f.hunks.clear();
f
})
.collect(),
);
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
let files = rows
.iter()
.filter(|r| matches!(r, DiffRow::FileHeader(_)))
.count();
assert_eq!(files, MAX_RENDERED_FILES);
assert!(matches!(rows.last(), Some(DiffRow::MoreFiles { rest: 25 })));
}
#[test]
fn the_untracked_list_is_capped_but_its_count_stays_true() {
let mut snap = snapshot(Vec::new());
snap.untracked = (0..MAX_RENDERED_FILES + 5)
.map(|i| format!("p{i}"))
.collect();
snap.untracked_total = 9_000;
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
assert!(matches!(
rows.first(),
Some(DiffRow::UntrackedHeader { total: 9_000 })
));
let shown = rows
.iter()
.filter(|r| matches!(r, DiffRow::Untracked { .. }))
.count();
assert_eq!(shown, MAX_RENDERED_FILES);
assert!(matches!(
rows.last(),
Some(DiffRow::MoreUntracked { rest }) if *rest == 9_000 - MAX_RENDERED_FILES
));
}
#[test]
fn a_preview_is_one_open_file() {
let rows = preview_rows(&file("new.md", 1), DiffViewMode::Unified);
assert_eq!(
shape(&rows),
["file", "hunk", "unified", "unified", "unified", "unified"]
);
let DiffRow::FileHeader(head) = &rows[0] else {
panic!("a file opens with its header");
};
assert_eq!(
head.index,
usize::MAX,
"its element ids stay clear of the real list's"
);
assert!(head.expanded);
}
#[test]
fn a_rename_shows_both_names_and_a_binary_file_cannot_be_opened() {
let mut renamed = file("new.rs", 1);
renamed.old_path = Some("old.rs".to_string());
let rows = preview_rows(&renamed, DiffViewMode::Unified);
let DiffRow::FileHeader(head) = &rows[0] else {
unreachable!()
};
assert_eq!(head.shown_path, "old.rs → new.rs");
assert_eq!(head.path, "new.rs", "the key is still the file itself");
let mut binary = file("logo.png", 0);
binary.binary = true;
let rows = preview_rows(&binary, DiffViewMode::Unified);
let DiffRow::FileHeader(head) = &rows[0] else {
unreachable!()
};
assert!(!head.expandable);
}
/// The threshold that decides whether a file opens on its own, kept where
/// the flattening that reads it lives.
#[test]
fn a_file_over_the_line_count_opens_only_when_asked() {
let mut big = file("big.rs", 1);
big.added = AUTO_COLLAPSE_LINES + 1;
let none = HashMap::new();
assert!(!file_expanded(&big, &none, false));
assert!(file_expanded(&big, &open(["big.rs"]), true));
assert!(file_expanded(&file("small.rs", 1), &none, false));
}
}
+1188 -744
View File
File diff suppressed because it is too large Load Diff
+9 -14
View File
@@ -24,6 +24,7 @@ pub(crate) enum Side {
New,
}
#[derive(PartialEq, Eq)]
pub(crate) struct SplitCell {
pub(crate) no: Option<u32>,
pub(crate) text: String,
@@ -34,6 +35,7 @@ pub(crate) struct SplitCell {
pub(crate) line: usize,
}
#[derive(PartialEq, Eq)]
pub(crate) struct SplitRow {
pub(crate) left: Option<SplitCell>,
pub(crate) right: Option<SplitCell>,
@@ -98,6 +100,7 @@ pub(crate) fn split_hunk(lines: &[DiffLine]) -> Vec<SplitRow> {
rows
}
#[derive(PartialEq, Eq)]
pub(crate) struct UnifiedRow {
pub(crate) old: Option<u32>,
pub(crate) new: Option<u32>,
@@ -139,17 +142,6 @@ impl HunkRows {
DiffViewMode::Unified => Self::Unified(unified_rows(lines)),
}
}
pub(crate) fn len(&self) -> usize {
match self {
Self::Split(rows) => rows.len(),
Self::Unified(rows) => rows.len(),
}
}
pub(crate) fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// Where a row sits in a file's card: which hunk, and which row inside it.
@@ -565,11 +557,14 @@ mod tests {
);
}
/// The one place a view mode turns into rows, so a copy is read off the
/// rows the list drew rather than a second guess at them.
#[test]
fn each_view_builds_the_rows_its_renderer_draws() {
let lines = hunk();
assert_eq!(HunkRows::build(DiffViewMode::Split, &lines).len(), 4);
assert_eq!(HunkRows::build(DiffViewMode::Unified, &lines).len(), 5);
assert!(HunkRows::build(DiffViewMode::Split, &[]).is_empty());
let split = HunkRows::build(DiffViewMode::Split, &lines);
assert!(matches!(split, HunkRows::Split(rows) if rows.len() == 4));
let unified = HunkRows::build(DiffViewMode::Unified, &lines);
assert!(matches!(unified, HunkRows::Unified(rows) if rows.len() == 5));
}
}
+1
View File
@@ -1,6 +1,7 @@
pub mod app;
pub mod assets;
pub mod code_editor;
pub mod diff_list;
pub mod diff_overlay;
pub mod diff_rows;
pub mod document_column;
+38 -1
View File
@@ -960,6 +960,7 @@ mod tests {
fn native_spec(user: &str, host: &str, port: u16) -> NativeSshSpec {
let mut profile = crate::core::ssh_profile::SshProfile::new(host.to_string());
profile.host = host.to_string();
profile.user = user.to_string();
profile.port = port;
crate::ui::ssh_connect::build_native_ssh_spec(
@@ -970,6 +971,36 @@ mod tests {
)
}
/// `SshProfile::new` takes a *name*, and the address lives in a separate
/// `host` field; every production caller assigns both. `native_spec` used
/// to assign only the name, so the spec it handed back addressed nobody
/// and `ConnectionKey::from_spec` spelled it `me@:22` — one key for every
/// machine in this module that talks to port 22.
///
/// `ORIGINS` is keyed by exactly that string, so the two tests below that
/// note an origin and read it back were writing to and reading from the
/// same slot. Whichever noted last won, and the other was handed the wrong
/// machine: 17 failures in 20 runs of this module alone, 6 in 20 of the
/// whole binary, and none when either test ran by itself.
#[test]
fn two_machines_do_not_share_one_route_origin_key() {
use crate::daemon::router::RouteTarget;
let build = RouteTarget::Ssh(Box::new(native_spec("me", "build-box", 22)));
let twin = RouteTarget::Ssh(Box::new(native_spec("me", "twin-box", 22)));
assert_eq!(
build.origin_key(),
"me@build-box:22",
"a route origin key names the machine it dials"
);
assert_ne!(
build.origin_key(),
twin.origin_key(),
"two machines sharing one key make `note_origin` overwrite the other's"
);
}
#[test]
fn a_routed_auth_prompt_carries_the_machine_that_raised_it() {
let _turn = claim_mailbox();
@@ -978,6 +1009,7 @@ mod tests {
let route =
crate::daemon::router::RouteTarget::Ssh(Box::new(native_spec("me", "build-box", 22)));
note_origin(&route, &target);
let origin_key = route.origin_key();
let handle = std::thread::spawn(move || {
use crate::daemon::router::RouteAuthResponder as _;
@@ -1005,7 +1037,12 @@ mod tests {
);
std::thread::sleep(Duration::from_millis(5));
};
assert_eq!(pending.host, target.host_id());
assert_eq!(
pending.host,
target.host_id(),
"the prompt names the machine noted under {:?}",
origin_key
);
pending.answer(AuthResponse::Secret("hunter2".into()));
assert_eq!(
handle.join().unwrap(),
-34
View File
@@ -40,24 +40,6 @@ pub(crate) fn segment_corners(
}
}
pub(crate) fn stack_corners(
i: usize,
count: usize,
outer: Pixels,
border: Pixels,
) -> Corners<Pixels> {
let r = inner_radius(outer, border);
let zero = px(0.);
let first = i < count && i == 0;
let last = i < count && i + 1 == count;
Corners {
top_left: if first { r } else { zero },
top_right: if first { r } else { zero },
bottom_left: if last { r } else { zero },
bottom_right: if last { r } else { zero },
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -109,20 +91,4 @@ mod tests {
Corners::all(px(0.))
);
}
#[test]
fn a_stack_caps_its_first_and_last_band() {
let r = inner_radius(CARD_RADIUS, HAIRLINE);
let zero = px(0.);
let top = stack_corners(0, 2, CARD_RADIUS, HAIRLINE);
assert_eq!((top.top_left, top.top_right), (r, r));
assert_eq!((top.bottom_left, top.bottom_right), (zero, zero));
let bottom = stack_corners(1, 2, CARD_RADIUS, HAIRLINE);
assert_eq!((bottom.bottom_left, bottom.bottom_right), (r, r));
assert_eq!((bottom.top_left, bottom.top_right), (zero, zero));
assert_eq!(stack_corners(0, 1, CARD_RADIUS, HAIRLINE), Corners::all(r));
}
}
+5 -5
View File
@@ -1,5 +1,5 @@
use gpui::{AnyElement, ElementId, Pixels, ScrollHandle, div, prelude::*, px};
use gpui_component::scroll::Scrollbar;
use gpui_component::scroll::{Scrollbar, ScrollbarHandle};
use gpui_component::v_flex;
/// Overlays the shared vertical scrollbar on a scroll area.
@@ -9,10 +9,10 @@ use gpui_component::v_flex;
/// dropping it straight into whatever is around it. Without that the wrapper
/// sizes to its content, the `size_full` scroll area inside grows with it, and
/// the pane stops scrolling because nothing overflows any more.
pub(crate) fn with_vertical_scrollbar(
pub(crate) fn with_vertical_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_area: impl IntoElement,
handle: &ScrollHandle,
handle: &H,
) -> AnyElement {
with_inset_vertical_scrollbar(id, scroll_area, handle, px(0.))
}
@@ -23,10 +23,10 @@ pub(crate) fn with_vertical_scrollbar(
/// the border is already the edge. A scroll area that *is* the window has no
/// such edge, and a bar that runs to the last pixel lands on the rounded
/// corner and reads as if it had been clipped.
pub(crate) fn with_inset_vertical_scrollbar(
pub(crate) fn with_inset_vertical_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_area: impl IntoElement,
handle: &ScrollHandle,
handle: &H,
inset_y: Pixels,
) -> AnyElement {
v_flex()