mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
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:
@@ -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
@@ -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
@@ -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]`
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+9
-14
@@ -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,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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user