Compare commits

...

26 Commits

Author SHA1 Message Date
Joseph T. Lyons
1516ee3e46 zed 0.177.1 2025-03-04 13:04:11 -05:00
gcp-cherry-pick-bot[bot]
53af68aa82 git: Fix project diff shortcuts (cherry-pick #26045) (#26049)
Cherry-picked git: Fix project diff shortcuts (#26045)

Release Notes:

- git: Fix keyboard shortcut display in project diff view
- vim: add git keyboard shortcuts: `d u/d U` for staging/unstaging in
the project diff view. `d o/d O` to show hide/toggle staged in the
editor and `d p` for restoring the hunk.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-04 13:01:18 -05:00
gcp-cherry-pick-bot[bot]
e897f191f6 Git Beta: Fix a few cases of empty toasts showing up (cherry-pick #25985) (#25987)
Cherry-picked Git Beta: Fix a few cases of empty toasts showing up
(#25985)

Improve parsing of git remote outputs

Release Notes:

- N/A

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-04 10:36:06 -07:00
gcp-cherry-pick-bot[bot]
aba10b73d2 Git fix repo selection (cherry-pick #25996) (#25998)
Cherry-picked Git fix repo selection (#25996)

Release Notes:

- git: Fixed a bug where staging/unstaging of hunks could use the wrong
git repository if you had many open

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-04 10:35:53 -07:00
gcp-cherry-pick-bot[bot]
46190bd087 git: Fix race condition loading project diff (cherry-pick #25992) (#25999)
Cherry-picked git: Fix race condition loading project diff (#25992)

Release Notes:

- git: Fixed a race condition where some files would be missing from
project diff

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-04 10:35:42 -07:00
Nate Butler
0b360febad git: Add hunk_style setting (#26038)
This PR adds the `git.hunk_style` setting, allowing setting an alternate
style for hunks – specifically the rendering of unstaged hunks.

It has 2 options:

- `transparent` (unstaged hunks are more transparent/less opaque than
staged hunks)
- `pattern (unstaged hunks are indicated by a visual pattern)

We'll possibly explore a VSCode-style "don't show staged hunks", but the
complexity it adds is a bit out of scope for now.

Transparent:

![CleanShot 2025-03-04 at 09 07
09@2x](https://github.com/user-attachments/assets/a74c4286-8264-48a2-bd58-0c582efb4e22)

Pattern:

![CleanShot 2025-03-04 at 09 10
12@2x](https://github.com/user-attachments/assets/4dd3040e-fb36-4670-9279-fcc7a4f12ced)

Release Notes:

- Git Beta: Added `git.hunk_style` setting to allow toggling between git
hunk visual styles.
2025-03-04 11:12:36 -05:00
gcp-cherry-pick-bot[bot]
11d75c42f1 Disable diff hunks for untracked files, even w/ no newline at eof (cherry-pick #25980) (#26004)
Cherry-picked Disable diff hunks for untracked files, even w/ no newline
at eof (#25980)

This fixes an issue where diff hunks were shown for untracked files, but
only if the files did not end with a newline.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-04 07:56:49 -08:00
Joseph T. Lyons
b3de2bf740 Reuse existing logic used to generate commit messages to disable commit buttons (#26034)
Also
- Recomputes `suggested_commit_message` and no longer stores it, to
ensure things are always up to date
- Reduces indentation in `render_footer`

Release Notes:

- N/A
2025-03-04 09:06:17 -05:00
Cole Miller
b2f174a622 Revert "git: Use worktree paths in the panel (#25950)" (#25995)
This reverts commit e7b3b8bf03.

Release Notes:

- N/A
2025-03-04 09:06:10 -05:00
gcp-cherry-pick-bot[bot]
a3b7c1d9e3 Fix a panic on Linux theme appearance change (cherry-pick #26019) (#26028)
Cherry-picked Fix a panic on Linux theme appearance change (#26019)

Closes https://github.com/zed-industries/zed/issues/26009



21484a2e9d/crates/gpui/src/platform/linux/platform.rs (L517-L519)

`with_common` panicked at `borrow_mut` which is the way it's implemented
for X11, Wayland and Headless Linux counterparts.



21484a2e9d/crates/gpui/src/platform/linux/wayland/client.rs (L722-L724)

By accessing the appearance global instead of a `RefCell` with it, the
panic goes away with one notable side-effect, on Linux only: the first
global's value on `Dark` appearance would be `Light`: it becomes normal
instantly, thanks to



21484a2e9d/crates/workspace/src/workspace.rs (L1083-L1090)

Things work without flickering:



[linux_theme_toggle.webm](https://github.com/user-attachments/assets/0e39ddc0-b4ff-4475-93ff-7b2bd7233628)


Release Notes:

- Fixed a panic on Linux theme appearance change

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-04 15:02:10 +02:00
Joseph T. Lyons
f4b83d1fba Make suggested commits placeholders and allow them to be committed (#26006)
This does not fix the bug where, when the commit editor modal is open,
changing the staged file does not update the suggested message in the
commit editor. Conrad mentioned he thought we shouldn't be allowed to
change those when the modal is open, so I'm not attempting to fix that.

Release Notes:

- Made suggested commits placeholders and allow them to be committed.
2025-03-04 02:02:50 -05:00
gcp-cherry-pick-bot[bot]
5de7f1bcd5 Skip .git/lfs FS events (cherry-pick #25927) (#26005)
Cherry-picked Skip .git/lfs FS events (#25927)

Closes https://github.com/zed-industries/zed/issues/25865
Closes https://github.com/zed-industries/zed/pull/25915

In the issue, Zed had caused `.git/lfs/tmp/466102258`-like files to
appear in the directory, which lead to background FS event listener to
handle this as an update, incrementing snapshot's `scan_id`, which lead
to git status rescan, which caused another increment to `status_scan_id`
— incrementing either of the IDs causes the related repo data to be
considered "changed:



41b45eaba7/crates/worktree/src/worktree.rs (L1590-L1605)

hence propagating events to the other parts of the system (e.g. git
blame, which was also active in the issue's case)

```
[2025-03-01T20:01:08+01:00 DEBUG worktree] ignoring event ".git/lfs/tmp/466102258" within unloaded directory
[2025-03-01T20:01:08+01:00 DEBUG worktree] received fs events []
[2025-03-01T20:01:08+01:00 DEBUG worktree] reloading repositories: ["/Users/alex/dev/monorepo/.git"]
[2025-03-01T20:01:08+01:00 DEBUG editor::git::blame] Status of git repositories updated. Regenerating blame data...
[2025-03-01T20:01:08+01:00 DEBUG editor::git::blame] Status of git repositories updated. Regenerating blame data...
[2025-03-01T20:01:08+01:00 DEBUG editor::git::blame] Status of git repositories updated. Regenerating blame data...
```

Due to repo update events sent, another `.git/lfs/tmp/` entry is
created, things start over...

The PR fixes this by ignoring any `.git/lfs/` directory-related FS
events, as needed for the current git status update heuristics.

https://github.com/zed-industries/zed/pull/25915 tried to follow further
and `scan_id` and `status_scan_id` but we do not store all git state in
memory, e.g. head


e0060b92cc/crates/editor/src/editor_tests.rs (L13686)
as

[tests](https://github.com/zed-industries/zed/actions/runs/13631960559/job/38101504549?pr=25915)
show.

Release Notes:

- Improved `.git` scan heuristics

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-04 08:38:18 +02:00
gcp-cherry-pick-bot[bot]
375885e6ec Disable Git panel button to open commit editor in certain cases (cherry-pick #26000) (#26001)
Cherry-picked Disable Git panel button to open commit editor in certain
cases (#26000)

Also:

- Internally renames a bit of code to make it easy to identify between
when we are disabling the buttons that open and close the modal editor
(in Git Panel and Project Diff) vs when we are disabling the commit
buttons (in Git Panel and Git commit editor modal).
- Deletes some unused code.

Release Notes:

- Unified disabling / enabling the button to open the Git commit editor
modal in the Git panel with the Project Diff commit button.
- Unified disabling / enabling the commit buttons, for the same cases,
between the Git panel and Git commit editor modal.

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-03-04 01:03:07 -05:00
gcp-cherry-pick-bot[bot]
7ab9ec904e git: New enter behaviour (cherry-pick #25986) (#25993)
Cherry-picked git: New enter behaviour (#25986)

Closes #25951

Release Notes:

- git: Update "enter" in the list of changed files to preserve focus. If
you want the old behaviour, hit enter twice.
- git: Follow the cursor, not the scroll anchor, in the list. Although
the scroll anchor was nice for passive scrolling, it broke if you had
changed the overflow scroll settings.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-03 21:40:46 -07:00
gcp-cherry-pick-bot[bot]
94425051a1 Improve consistency with commit button tooltip between Git panel and modal (cherry-pick #25990) (#25991)
Cherry-picked Refactor more code around commit button text (#25990)

Missed this when doing https://github.com/zed-industries/zed/pull/25988

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-03-03 22:55:26 -05:00
gcp-cherry-pick-bot[bot]
7bd4a85a29 Use same commit button text in panel and modal (cherry-pick #25988) (#25989)
Cherry-picked Use same commit button text in panel and modal (#25988)

Release Notes:

- Fixed inconsistencies in commit button text between Git panel and
modal.

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-03-03 22:32:57 -05:00
gcp-cherry-pick-bot[bot]
7d1b50ea85 vim: Fix key navigation on folded buffer headers (cherry-pick #25944) (#25972)
Cherry-picked vim: Fix key navigation on folded buffer headers (#25944)

Closes #24243

Release Notes:

- vim: Fix j/k on folded multibuffer headers

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: João Marcos <marcospb19@hotmail.com>
2025-03-03 19:40:24 -07:00
gcp-cherry-pick-bot[bot]
96ce87d2dd Fix toggle fold in deleted hunk (cherry-pick #25967) (#25982)
Cherry-picked Fix toggle fold in deleted hunk (#25967)

Updates #25835
Updates #25951

Closes #ISSUE

Release Notes:

- Fixed toggling folds from within deleted hunks

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-03 19:39:52 -07:00
Max Brunsfeld
1c6bf1f9b1 Show git panel footer even when on a detached HEAD (#25968)
Previously, the git panel footer would accidentally hide when not on a
branch.

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
2025-03-03 16:46:14 -08:00
Cole Miller
8d9d14c2b9 git: Use worktree paths in the panel (#25950)
This PR changes the git panel to use worktree-relative paths for its
entries, instead of repository-relative paths as before. Paths that lie
outside the active repository's worktree are no longer shown in the
panel. Note that in both respects this is how the project diff editor
already works, so this PR brings those two pieces of UI into harmony.

Release Notes:

- N/A
2025-03-03 19:16:25 -05:00
Nate Butler
46944b679f git_ui: horizontal is not vertical (#25961)
Fixes an issue where I was missing some brain cells and changed the git
panel's `render_entries` to a `v_flex` instead of an `h_flex`.

But actually, fixes the git panel entries from disappearing when a
scrollbar is rendered.

**Before**

![CleanShot 2025-03-03 at 16 36
52@2x](https://github.com/user-attachments/assets/9dca7b9c-318d-4b3f-ab3e-e7242fa7f73a)

**After**

![CleanShot 2025-03-03 at 16 35
59@2x](https://github.com/user-attachments/assets/c1fe5fb1-ad57-4bca-ace4-365e70a74066)


Closes #25955

Release Notes:

- Git Beta: Fixed an issue where when the git panel would need to scroll
all the items are pushed off the screen.
2025-03-03 19:15:20 -05:00
Nate Butler
b1386bff7b git_ui: Prevent button overflow due to long names (#25940)
- Fix component preview widths for git panel
- Fix buttons getting pushed off the screen in git  panel

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-03 19:14:37 -05:00
gcp-cherry-pick-bot[bot]
02204dee06 git: Don't consider $HOME as containing git repository unless it's opened directly (cherry-pick #25948) (#25952)
Cherry-picked git: Don't consider $HOME as containing git repository
unless it's opened directly (#25948)

When a worktree is created, we walk up the ancestors of the root path
trying to find a git repository. In particular, if your `$HOME` is a git
repository and you open some subdirectory of `$HOME` that's *not* a git
repository, we end up scanning `$HOME` and everything under it looking
for changed and untracked files, which is often pretty slow. Consistency
here is not very useful and leads to a bad experience.

This PR adds a special case to not consider `$HOME` as a containing git
repository, unless you ask for it by doing the equivalent of `zed ~`.

Release Notes:

- Changed the behavior of git features to not treat `$HOME` as a git
repository unless opened directly

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-03 17:38:11 -05:00
gcp-cherry-pick-bot[bot]
a1e613805a Add some logging to debug missing parent git repositories (cherry-pick #25943) (#25946)
Cherry-picked Add some logging to debug missing parent git repositories
(#25943)

We've had some issues reported with git repositories not getting
detected when they're a strict parent of the worktree root. Add a bit
more logging to understand what's going on here.

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-03 14:25:18 -05:00
gcp-cherry-pick-bot[bot]
5852f2e0a4 Fix missing hunks in project diff after revert (cherry-pick #25906) (#25947)
Cherry-picked Fix missing hunks in project diff after revert (#25906)

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-03 14:25:05 -05:00
Joseph T. Lyons
3130b46515 v0.177.x preview 2025-03-03 12:42:43 -05:00
54 changed files with 1448 additions and 725 deletions

131
Cargo.lock generated
View File

@@ -1178,9 +1178,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.5.17"
version = "1.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490aa7465ee685b2ced076bb87ef654a47724a7844e2c7d3af4e749ce5b875dd"
checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1271,9 +1271,9 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.75.0"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ddf7475b6f50a1a5be8edb1bcdf6e4ae00feed5b890d14a3f1f0e14d76f5a16"
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1295,9 +1295,9 @@ dependencies = [
[[package]]
name = "aws-sdk-kinesis"
version = "1.62.0"
version = "1.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31622345afd0c35d33c1cbba73ccf9fb88e09857413d8963dea2c493e00704d"
checksum = "89f2163d8704e8fdcd51ec6c2e0441c418471e422ee9690451b17a1c46344e1a"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1317,9 +1317,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.77.0"
version = "1.76.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34e87342432a3de0e94e82c99a7cbd9042f99de029ae1f4e368160f9e9929264"
checksum = "66e83401ad7287ad15244d557e35502c2a94105ca5b41d656c391f1a4fc04ca2"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1351,9 +1351,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.60.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60186fab60b24376d3e33b9ff0a43485f99efd470e3b75a9160c849741d63d56"
checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1373,9 +1373,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7033130ce1ee13e6018905b7b976c915963755aef299c1521897679d6cd4f8ef"
checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1395,9 +1395,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5c1cac7677179d622b4448b0d31bcb359185295dc6fca891920cfb17e2b5156"
checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1458,9 +1458,9 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.63.0"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2dc8d842d872529355c72632de49ef8c5a2949a4472f10e802f28cf925770c"
checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@@ -1810,7 +1810,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -1833,7 +1833,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
@@ -2404,6 +2404,25 @@ dependencies = [
"cipher",
]
[[package]]
name = "cbindgen"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
dependencies = [
"clap",
"heck 0.4.1",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.90",
"tempfile",
"toml 0.8.20",
]
[[package]]
name = "cbindgen"
version = "0.28.0"
@@ -2501,9 +2520,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.40"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -2511,7 +2530,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-targets 0.52.6",
]
[[package]]
@@ -3508,10 +3527,11 @@ dependencies = [
[[package]]
name = "crc64fast-nvme"
version = "1.2.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37"
dependencies = [
"cbindgen 0.27.0",
"crc",
]
@@ -5400,8 +5420,10 @@ dependencies = [
"buffer_diff",
"collections",
"component",
"ctor",
"db",
"editor",
"env_logger 0.11.6",
"feature_flags",
"futures 0.3.31",
"fuzzy",
@@ -5563,7 +5585,7 @@ dependencies = [
"bytemuck",
"calloop",
"calloop-wayland-source",
"cbindgen",
"cbindgen 0.28.0",
"cocoa 0.26.0",
"collections",
"core-foundation 0.9.4",
@@ -7236,9 +7258,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libdbus-sys"
@@ -9765,15 +9787,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "pgvector"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
dependencies = [
"serde",
]
[[package]]
name = "phf"
version = "0.11.2"
@@ -10413,7 +10426,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.0",
"heck 0.5.0",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10446,7 +10459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -11474,9 +11487,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -11485,9 +11498,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
dependencies = [
"proc-macro2",
"quote",
@@ -11498,9 +11511,9 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
dependencies = [
"globset",
"sha2",
@@ -11775,9 +11788,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"indexmap",
@@ -11788,9 +11801,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
@@ -11853,18 +11866,17 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fba7b2c749b2d0a00303d5cb13e6761e39a4172554bdf930852cac4e7aeabd"
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
dependencies = [
"async-stream",
"async-trait",
"bigdecimal",
"chrono",
"futures-util",
"futures 0.3.31",
"log",
"ouroboros",
"pgvector",
"rust_decimal",
"sea-orm-macros",
"sea-query",
@@ -11882,9 +11894,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2568cff8d35d5150b4276cc0dd766192a587f64b6ece60ae3706e0872c4eb209"
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -14804,9 +14816,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.15.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
dependencies = [
"getrandom 0.3.1",
"serde",
@@ -14908,6 +14920,7 @@ dependencies = [
"multi_buffer",
"nvim-rs",
"parking_lot",
"project",
"project_panel",
"regex",
"release_channel",
@@ -15929,12 +15942,6 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -16770,7 +16777,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.177.0"
version = "0.177.1"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -370,10 +370,10 @@
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": ["git::StageAndNext", { "whole_excerpt": false }],
"alt-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
"alt-.": ["editor::GoToHunk", { "center_cursor": true }],
"alt-,": ["editor::GoToPreviousHunk", { "center_cursor": true }]
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
},
{
@@ -564,8 +564,8 @@
"shift-enter": "editor::ExpandExcerpts",
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": ["editor::GoToHunk", { "center_cursor": true }],
"ctrl-shift-f8": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPreviousHunk",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-:": "editor::ToggleInlayHints"
}
@@ -722,7 +722,7 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"ctrl-enter": "git::ShowCommitEditor",
"alt-enter": "menu::SecondaryConfirm"
}
},
@@ -736,7 +736,7 @@
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit"
"ctrl-enter": "git::ShowCommitEditor"
}
},
{
@@ -749,14 +749,6 @@
"alt-up": "git_panel::FocusChanges"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"ctrl-enter": "git::Commit"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {

View File

@@ -142,8 +142,8 @@
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": ["git::StageAndNext", { "whole_excerpt": false }],
"cmd-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame",
@@ -642,8 +642,8 @@
"shift-enter": "editor::ExpandExcerpts",
"cmd-alt-enter": "editor::OpenExcerptsSplit",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-f8": ["editor::GoToHunk", { "center_cursor": true }],
"cmd-shift-f8": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPreviousHunk",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-:": "editor::ToggleInlayHints"
}
@@ -743,14 +743,14 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit"
"cmd-enter": "git::ShowCommitEditor"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit"
"cmd-enter": "git::ShowCommitEditor"
}
},
{

View File

@@ -42,8 +42,8 @@
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPreviousDiagnostic",
"ctrl-alt-shift-down": ["editor::GoToHunk", { "center_cursor": true }],
"ctrl-alt-shift-up": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
"ctrl-alt-z": "git::Restore",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",

View File

@@ -43,8 +43,8 @@
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": ["editor::GoToHunk", { "center_cursor": true }],
"ctrl-,": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",

View File

@@ -40,8 +40,8 @@
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPreviousDiagnostic",
"ctrl-alt-shift-down": ["editor::GoToHunk", { "center_cursor": true }],
"ctrl-alt-shift-up": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
"cmd-home": "editor::MoveToBeginning",
"cmd-end": "editor::MoveToEnd",
"cmd-shift-home": "editor::SelectToBeginning",

View File

@@ -44,8 +44,8 @@
"alt-cmd-down": "editor::GoToDefinition",
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
"alt-shift-cmd-down": "editor::FindAllReferences",
"ctrl-.": ["editor::GoToHunk", { "center_cursor": true }],
"ctrl-,": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",

View File

@@ -238,8 +238,8 @@
"] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": ["editor::GoToHunk", { "center_cursor": true }],
"[ c": ["editor::GoToPreviousHunk", { "center_cursor": true }],
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
"g c": "vim::PushToggleComments"
}
},
@@ -448,7 +448,10 @@
"d": "vim::CurrentLine",
"s": "vim::PushDeleteSurrounds",
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"p": "git::Restore" // "d p"
"shift-o": "git::ToggleStaged",
"p": "git::Restore", // "d p"
"u": "git::StageAndNext", // "d u"
"shift-u": "git::UnstageAndNext" // "d shift-u"
}
},
{

View File

@@ -837,7 +837,15 @@
//
// The minimum column number to show the inline blame information at
// "min_column": 0
}
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
// 1. Show unstaged hunks with a transparent background (default):
// "hunk_style": "transparent"
// 2. Show unstaged hunks with a pattern background:
// "hunk_style": "pattern"
"hunk_style": "transparent"
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.
@@ -851,15 +859,7 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.

View File

@@ -609,7 +609,7 @@ impl AssistantPanel {
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).text_ellipsis()),
.child(Label::new(title).truncate()),
)
.child(
h_flex()

View File

@@ -260,7 +260,7 @@ impl RenderOnce for PastThread {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()
@@ -356,7 +356,7 @@ impl RenderOnce for PastContext {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()

View File

@@ -243,7 +243,7 @@ impl PickerDelegate for SlashCommandDelegate {
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.text_ellipsis(),
.truncate(),
),
),
),

View File

@@ -56,8 +56,8 @@ pub enum DiffHunkSecondaryStatus {
/// A diff hunk resolved to rows in the buffer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffHunk {
/// The buffer range, expressed in terms of rows.
pub row_range: Range<u32>,
/// The buffer range as points.
pub range: Range<Point>,
/// The range in the buffer to which this hunk corresponds.
pub buffer_range: Range<Anchor>,
/// The range in the buffer's diff base text to which this hunk corresponds.
@@ -362,6 +362,7 @@ impl BufferDiffInner {
pending_hunks = secondary.pending_hunks.clone();
}
let max_point = buffer.max_point();
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || loop {
let (start_point, (start_anchor, start_base)) = summaries.next()?;
@@ -371,7 +372,7 @@ impl BufferDiffInner {
continue;
}
if end_point.column > 0 {
if end_point.column > 0 && end_point < max_point {
end_point.row += 1;
end_point.column = 0;
end_anchor = buffer.anchor_before(end_point);
@@ -416,7 +417,7 @@ impl BufferDiffInner {
}
return Some(DiffHunk {
row_range: start_point.row..end_point.row,
range: start_point..end_point,
diff_base_byte_range: start_base..end_base,
buffer_range: start_anchor..end_anchor,
secondary_status,
@@ -442,14 +443,9 @@ impl BufferDiffInner {
let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
} else {
range.end.row
};
Some(DiffHunk {
row_range: range.start.row..end_row,
range,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
@@ -1136,12 +1132,10 @@ pub fn assert_hunks<Iter>(
let actual_hunks = diff_hunks
.map(|hunk| {
(
hunk.row_range.clone(),
hunk.range.clone(),
&diff_base[hunk.diff_base_byte_range.clone()],
buffer
.text_for_range(
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
)
.text_for_range(hunk.range.clone())
.collect::<String>(),
hunk.status(),
)
@@ -1150,7 +1144,14 @@ pub fn assert_hunks<Iter>(
let expected_hunks: Vec<_> = expected_hunks
.iter()
.map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
.map(|(r, old_text, new_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
*old_text,
new_text.to_string(),
*status,
)
})
.collect();
assert_eq!(actual_hunks, expected_hunks);

View File

@@ -226,3 +226,7 @@ impl Item for ComponentPreview {
f(*event)
}
}
// TODO: impl serializable item for component preview so it will restore with the workspace
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler

View File

@@ -196,20 +196,6 @@ pub struct DeleteToPreviousWordStart {
pub ignore_newlines: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GoToHunk {
#[serde(default)]
pub center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GoToPreviousHunk {
#[serde(default)]
pub center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
pub struct FoldAtLevel(pub u32);
@@ -240,8 +226,6 @@ impl_actions!(
ExpandExcerptsDown,
ExpandExcerptsUp,
FoldAt,
GoToHunk,
GoToPreviousHunk,
HandleInput,
MoveDownByLines,
MovePageDown,
@@ -323,6 +307,8 @@ gpui::actions!(
GoToDefinition,
GoToDefinitionSplit,
GoToDiagnostic,
GoToHunk,
GoToPreviousHunk,
GoToImplementation,
GoToImplementationSplit,
GoToPreviousDiagnostic,

View File

@@ -1124,6 +1124,11 @@ impl DisplaySnapshot {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
self.block_snapshot
.is_folded_buffer_header(BlockRow(display_row.0))
}
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
let wrap_row = self
.block_snapshot

View File

@@ -1618,6 +1618,15 @@ impl BlockSnapshot {
cursor.item().map_or(false, |t| t.block.is_some())
}
pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&row, Bias::Right, &());
let Some(transform) = cursor.item() else {
return false;
};
matches!(transform.block, Some(Block::FoldedBuffer { .. }))
}
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
let wrap_point = self
.wrap_snapshot

View File

@@ -73,7 +73,7 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use ::git::{status::FileStatus, Restore};
use ::git::Restore;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
@@ -11412,14 +11412,13 @@ impl Editor {
}
}
fn go_to_next_hunk(&mut self, action: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
true,
action.center_cursor,
Direction::Next,
window,
cx,
);
@@ -11429,12 +11428,11 @@ impl Editor {
&mut self,
snapshot: &EditorSnapshot,
position: Point,
after: bool,
scroll_center: bool,
direction: Direction,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<MultiBufferDiffHunk> {
let hunk = if after {
let hunk = if direction == Direction::Next {
self.hunk_after_position(snapshot, position)
} else {
self.hunk_before_position(snapshot, position)
@@ -11442,11 +11440,7 @@ impl Editor {
if let Some(hunk) = &hunk {
let destination = Point::new(hunk.row_range.start.0, 0);
let autoscroll = if scroll_center {
Autoscroll::center()
} else {
Autoscroll::fit()
};
let autoscroll = Autoscroll::center();
self.unfold_ranges(&[destination..destination], false, false, cx);
self.change_selections(Some(autoscroll), window, cx, |s| {
@@ -11476,7 +11470,7 @@ impl Editor {
fn go_to_prev_hunk(
&mut self,
action: &GoToPreviousHunk,
_: &GoToPreviousHunk,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -11485,8 +11479,7 @@ impl Editor {
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
false,
action.center_cursor,
Direction::Prev,
window,
cx,
);
@@ -12965,13 +12958,18 @@ impl Editor {
}
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_ids: HashSet<_> = multi_buffer_snapshot
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
.map(|(snapshot, _, _)| snapshot.remote_id())
let buffer_ids: HashSet<_> = self
.selections
.disjoint_anchor_ranges()
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
.collect();
let should_unfold = buffer_ids
.iter()
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
for buffer_id in buffer_ids {
if self.is_buffer_folded(buffer_id, cx) {
if should_unfold {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
@@ -13558,20 +13556,20 @@ impl Editor {
pub fn stage_and_next(
&mut self,
action: &::git::StageAndNext,
_: &::git::StageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_stage_or_unstage_and_next(true, action.whole_excerpt, window, cx);
self.do_stage_or_unstage_and_next(true, window, cx);
}
pub fn unstage_and_next(
&mut self,
action: &::git::UnstageAndNext,
_: &::git::UnstageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_stage_or_unstage_and_next(false, action.whole_excerpt, window, cx);
self.do_stage_or_unstage_and_next(false, window, cx);
}
pub fn stage_or_unstage_diff_hunks(
@@ -13593,102 +13591,33 @@ impl Editor {
fn do_stage_or_unstage_and_next(
&mut self,
stage: bool,
whole_excerpt: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
if ranges.iter().any(|range| range.start != range.end) {
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
return;
}
if !whole_excerpt {
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let run_twice = snapshot
.hunks_for_ranges([newest_range])
.first()
.is_some_and(|hunk| {
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
self.hunk_after_position(&snapshot, next_line)
.is_some_and(|other| other.row_range == hunk.row_range)
});
let run_twice = snapshot
.hunks_for_ranges([newest_range])
.first()
.is_some_and(|hunk| {
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
self.hunk_after_position(&snapshot, next_line)
.is_some_and(|other| other.row_range == hunk.row_range)
});
if run_twice {
self.go_to_next_hunk(
&GoToHunk {
center_cursor: true,
},
window,
cx,
);
}
} else if !self.buffer().read(cx).is_singleton() {
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) {
if buffer.read(cx).is_empty() {
let buffer = buffer.read(cx);
let Some(file) = buffer.file() else {
return;
};
let project_path = project::ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
};
let Some(project) = self.project.as_ref() else {
return;
};
let Some(repo) = project.read(cx).git_store().read(cx).active_repository()
else {
return;
};
repo.update(cx, |repo, cx| {
let Some(repo_path) = repo.project_path_to_repo_path(&project_path) else {
return;
};
let Some(status) = repo.repository_entry.status_for_path(&repo_path) else {
return;
};
if stage && status.status == FileStatus::Untracked {
repo.stage_entries(vec![repo_path], cx)
.detach_and_log_err(cx);
return;
}
})
}
ranges = vec![multi_buffer::Anchor::range_in_buffer(
excerpt_id,
buffer.read(cx).remote_id(),
range,
)];
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
let snapshot = self.buffer().read(cx).snapshot(cx);
let mut point = ranges.last().unwrap().end.to_point(&snapshot);
if point.row < snapshot.max_row().0 {
point.row += 1;
point.column = 0;
point = snapshot.clip_point(point, Bias::Right);
self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| {
s.select_ranges([point..point]);
});
}
return;
}
if run_twice {
self.go_to_next_hunk(&GoToHunk, window, cx);
}
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
self.go_to_next_hunk(
&GoToHunk {
center_cursor: true,
},
window,
cx,
);
self.go_to_next_hunk(&GoToHunk, window, cx);
}
fn do_stage_or_unstage(
@@ -13728,7 +13657,7 @@ impl Editor {
buffer_range: hunk.buffer_range,
diff_base_byte_range: hunk.diff_base_byte_range,
secondary_status: hunk.secondary_status,
row_range: 0..0, // unused
range: Point::zero()..Point::zero(), // unused
})
.collect::<Vec<_>>(),
&buffer_snapshot,
@@ -16036,9 +15965,9 @@ impl Editor {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes.into_iter().map(|(range, text)| {
(range, text.to_string().map(Arc::<str>::from))
}),
changes
.into_iter()
.map(|(range, text)| (range, text.to_string())),
None,
cx,
);
@@ -17156,17 +17085,14 @@ impl EditorSnapshot {
for hunk in self.buffer_snapshot.diff_hunks_in_range(
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk.status().is_deleted();
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
hunk.row_range.overlaps(&query_rows)
};
if related_to_selection {
// Include deleted hunks that are adjacent to the query range, because
// otherwise they would be missed.
let mut intersects_range = hunk.row_range.overlaps(&query_rows);
if hunk.status().is_deleted() {
intersects_range |= hunk.row_range.start == query_rows.end;
intersects_range |= hunk.row_range.end == query_rows.start;
}
if intersects_range {
if !processed_buffer_rows
.entry(hunk.buffer_id)
.or_default()

View File

@@ -11413,7 +11413,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
cx.update_editor(|editor, window, cx| {
//Wrap around the bottom of the buffer
for _ in 0..3 {
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
editor.go_to_next_hunk(&GoToHunk, window, cx);
}
});
@@ -11435,7 +11435,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
cx.update_editor(|editor, window, cx| {
//Wrap around the top of the buffer
for _ in 0..2 {
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
}
});
@@ -11455,7 +11455,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
);
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
});
cx.assert_editor_state(
@@ -11474,7 +11474,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
);
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
});
cx.assert_editor_state(
@@ -11494,7 +11494,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
cx.update_editor(|editor, window, cx| {
for _ in 0..2 {
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
}
});
@@ -11518,7 +11518,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
cx.update_editor(|editor, window, cx| {
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
editor.go_to_next_hunk(&GoToHunk, window, cx);
});
cx.assert_editor_state(
@@ -13525,7 +13525,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
editor.go_to_next_hunk(&GoToHunk, window, cx);
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();
@@ -13547,7 +13547,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
cx.update_editor(|editor, window, cx| {
for _ in 0..2 {
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
editor.go_to_next_hunk(&GoToHunk, window, cx);
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
}
});
@@ -13570,7 +13570,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
);
cx.update_editor(|editor, window, cx| {
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
editor.go_to_next_hunk(&GoToHunk, window, cx);
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();

View File

@@ -32,15 +32,17 @@ use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
Subscription, TextRun, TextStyleRefinement, Window,
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
TextStyleRefinement, Window,
};
use inline_completion::Direction;
use itertools::Itertools;
use language::{
language_settings::{
@@ -54,7 +56,7 @@ use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
use settings::Settings;
use smallvec::{smallvec, SmallVec};
use std::{
@@ -2722,7 +2724,10 @@ impl EditorElement {
.shadow_md()
.border_1()
.map(|div| {
let border_color = if is_selected && is_folded {
let border_color = if is_selected
&& is_folded
&& focus_handle.contains_focused(window, cx)
{
colors.border_focused
} else {
colors.border
@@ -4343,7 +4348,7 @@ impl EditorElement {
}
}
fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let is_light = cx.theme().appearance().is_light();
if layout.display_hunks.is_empty() {
@@ -4413,10 +4418,19 @@ impl EditorElement {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
// Flatten the background color with the editor color to prevent
// elements below transparent hunks from showing through
let flattened_background_color = cx
.theme()
.colors()
.editor_background
.blend(background_color);
window.paint_quad(quad(
hunk_bounds,
corner_radii,
background_color,
flattened_background_color,
Edges::default(),
transparent_black(),
));
@@ -4544,7 +4558,7 @@ impl EditorElement {
)
});
if show_git_gutter {
Self::paint_diff_hunks(layout, window, cx)
Self::paint_gutter_diff_hunks(layout, window, cx)
}
let highlight_width = 0.275 * layout.position_map.line_height;
@@ -6708,15 +6722,16 @@ impl Element for EditorElement {
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let is_light = cx.theme().appearance().is_light();
let use_pattern = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
continue;
};
let staged_opacity = if is_light { 0.14 } else { 0.10 };
let unstaged_opacity = 0.04;
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted => {
@@ -6727,15 +6742,34 @@ impl Element for EditorElement {
continue;
}
};
let background_color = if diff_status.has_secondary_hunk() {
background_color.opacity(unstaged_opacity)
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let staged_background =
solid_background(background_color.opacity(hunk_opacity));
let unstaged_background = if use_pattern {
pattern_slash(
background_color.opacity(hunk_opacity),
window.rem_size().0 * 1.125, // ~18 by default
)
} else {
background_color.opacity(staged_opacity)
solid_background(background_color.opacity(if is_light {
0.08
} else {
0.04
}))
};
let background = if unstaged {
unstaged_background
} else {
staged_background
};
highlighted_rows
.entry(start_row + DisplayRow(ix as u32))
.or_insert(background_color.into());
.or_insert(background);
}
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
@@ -8873,7 +8907,7 @@ fn diff_hunk_controls(
move |window, cx| {
Tooltip::for_action_in(
"Next Hunk",
&GoToHunk::default(),
&GoToHunk,
&focus_handle,
window,
cx,
@@ -8888,7 +8922,11 @@ fn diff_hunk_controls(
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
&snapshot, position, true, true, window, cx,
&snapshot,
position,
Direction::Next,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
@@ -8905,7 +8943,7 @@ fn diff_hunk_controls(
move |window, cx| {
Tooltip::for_action_in(
"Previous Hunk",
&GoToPreviousHunk::default(),
&GoToPreviousHunk,
&focus_handle,
window,
cx,
@@ -8920,7 +8958,11 @@ fn diff_hunk_controls(
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
&snapshot, point, false, true, window, cx,
&snapshot,
point,
Direction::Prev,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});

View File

@@ -89,6 +89,16 @@ impl EditorTestContext {
Path::new("/root")
}
pub async fn for_editor_in(editor: Entity<Editor>, cx: &mut gpui::VisualTestContext) -> Self {
cx.focus(&editor);
Self {
window: cx.windows()[0],
cx: cx.clone(),
editor,
assertion_cx: AssertionContextManager::new(),
}
}
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
let editor_view = editor.root(cx).unwrap();
Self {
@@ -381,6 +391,76 @@ impl EditorTestContext {
assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
}
#[track_caller]
pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
let expected_excerpts = marked_text
.strip_prefix("[EXCERPT]\n")
.unwrap()
.split("[EXCERPT]\n")
.collect::<Vec<_>>();
let (selections, excerpts) = self.update_editor(|editor, _, cx| {
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
let selections = editor.selections.disjoint_anchors();
let excerpts = multibuffer_snapshot
.excerpts()
.map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
.collect::<Vec<_>>();
(selections, excerpts)
});
assert_eq!(excerpts.len(), expected_excerpts.len());
for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
let is_folded = self
.update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
let (expected_text, expected_selections) =
marked_text_ranges(expected_excerpts[ix], true);
if expected_text == "[FOLDED]\n" {
assert!(is_folded, "excerpt {} should be folded", ix);
let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
if expected_selections.len() > 0 {
assert!(
is_selected,
"excerpt {} should be selected. Got {:?}",
ix,
self.editor_state()
);
} else {
assert!(!is_selected, "excerpt {} should not be selected", ix);
}
continue;
}
assert!(!is_folded, "excerpt {} should not be folded", ix);
assert_eq!(
snapshot
.text_for_range(range.context.clone())
.collect::<String>(),
expected_text
);
let selections = selections
.iter()
.filter(|s| s.head().excerpt_id == excerpt_id)
.map(|s| {
let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
tail..head
})
.collect::<Vec<_>>();
// todo: selections that cross excerpt boundaries..
assert_eq!(
selections, expected_selections,
"excerpt {} has incorrect selections",
ix,
);
}
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
@@ -392,6 +472,17 @@ impl EditorTestContext {
self.assert_selections(expected_selections, marked_text.to_string())
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
pub fn assert_display_state(&mut self, marked_text: &str) {
let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn editor_state(&mut self) -> String {
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
}

View File

@@ -522,7 +522,7 @@ impl ExtensionsPage {
extension.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.child(Label::new("<>").size(LabelSize::Small)),
)
@@ -534,7 +534,7 @@ impl ExtensionsPage {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
.truncate()
}))
.children(repository_url.map(|repository_url| {
IconButton::new(
@@ -665,7 +665,7 @@ impl ExtensionsPage {
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.child(
Label::new(format!(
@@ -683,7 +683,7 @@ impl ExtensionsPage {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
.truncate()
}))
.child(
h_flex()

View File

@@ -135,6 +135,7 @@ pub trait Fs: Send + Sync {
Arc<dyn Watcher>,
);
fn home_dir(&self) -> Option<PathBuf>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
@@ -813,6 +814,10 @@ impl Fs for RealFs {
temp_dir.close()?;
case_sensitive
}
fn home_dir(&self) -> Option<PathBuf> {
Some(paths::home_dir().clone())
}
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
@@ -846,6 +851,7 @@ struct FakeFsState {
metadata_call_count: usize,
read_dir_call_count: usize,
moves: std::collections::HashMap<u64, PathBuf>,
home_dir: Option<PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -1031,6 +1037,7 @@ impl FakeFs {
read_dir_call_count: 0,
metadata_call_count: 0,
moves: Default::default(),
home_dir: None,
}),
});
@@ -1524,6 +1531,10 @@ impl FakeFs {
fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
self.executor.simulate_random_delay()
}
pub fn set_home_dir(&self, home_dir: PathBuf) {
self.state.lock().home_dir = Some(home_dir);
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -2079,6 +2090,10 @@ impl Fs for FakeFs {
fn as_fake(&self) -> Arc<FakeFs> {
self.this.upgrade().unwrap()
}
fn home_dir(&self) -> Option<PathBuf> {
self.state.lock().home_dir.clone()
}
}
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {

View File

@@ -36,23 +36,15 @@ pub struct Push {
pub options: Option<PushOptions>,
}
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct StageAndNext {
pub whole_excerpt: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct UnstageAndNext {
pub whole_excerpt: bool,
}
impl_actions!(git, [Push, StageAndNext, UnstageAndNext]);
impl_actions!(git, [Push]);
actions!(
git,
[
// per-hunk
ToggleStaged,
StageAndNext,
UnstageAndNext,
// per-file
StageFile,
UnstageFile,

View File

@@ -57,6 +57,8 @@ zed_actions.workspace = true
windows.workspace = true
[dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }

View File

@@ -115,15 +115,15 @@ impl CommitModal {
return;
};
let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
let can_commit = git_panel.can_commit();
let (can_open_commit_editor, conflict) = git_panel.update(cx, |git_panel, cx| {
let can_open_commit_editor = git_panel.can_open_commit_editor();
let conflict = git_panel.has_unstaged_conflicts();
if can_commit {
if can_open_commit_editor {
git_panel.set_modal_open(true, cx);
}
(can_commit, conflict)
(can_open_commit_editor, conflict)
});
if !can_commit {
if !can_open_commit_editor {
let message = if conflict {
"There are still conflicts. You must stage these before committing."
} else {
@@ -163,7 +163,7 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_message = panel.suggest_commit_message();
let suggested_commit_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
@@ -174,19 +174,11 @@ impl CommitModal {
let commit_message = commit_editor.read(cx).text(cx);
if let Some(suggested_message) = suggested_message {
if let Some(suggested_commit_message) = suggested_commit_message {
if commit_message.is_empty() {
commit_editor.update(cx, |editor, cx| {
editor.set_text(suggested_message, window, cx);
editor.select_all(&Default::default(), window, cx);
editor.set_placeholder_text(suggested_commit_message, cx);
});
} else {
if commit_message.as_str().trim() == suggested_message.trim() {
commit_editor.update(cx, |editor, cx| {
// select the message to make it easy to delete
editor.select_all(&Default::default(), window, cx);
});
}
}
}
@@ -250,7 +242,7 @@ impl CommitModal {
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let (branch, tooltip, commit_label, co_authors) =
let (branch, can_commit, tooltip, commit_label, co_authors) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
@@ -262,18 +254,10 @@ impl CommitModal {
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
} else {
"Commit changes to tracked files"
};
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit All"
};
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
(branch, can_commit, tooltip, title, co_authors)
});
let branch_picker_button = panel_button(branch)
@@ -308,9 +292,8 @@ impl CommitModal {
None
};
let (panel_editor_focus_handle, can_commit) = git_panel.update(cx, |git_panel, cx| {
(git_panel.editor_focus_handle(cx), git_panel.can_commit())
});
let panel_editor_focus_handle =
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
let commit_button = panel_filled_button(commit_label)
.tooltip(move |window, cx| {
@@ -354,6 +337,7 @@ impl CommitModal {
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
self.git_panel
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));

View File

@@ -191,7 +191,6 @@ pub struct GitPanel {
pending_remote_operations: RemoteOperations,
pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
pub(crate) suggested_commit_message: Option<String>,
conflicted_count: usize,
conflicted_staged_count: usize,
current_modifiers: Modifiers,
@@ -319,7 +318,6 @@ impl GitPanel {
remote_operation_id: 0,
active_repository,
commit_editor,
suggested_commit_message: None,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
@@ -696,12 +694,31 @@ impl GitPanel {
fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
maybe!({
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
let workspace = self.workspace.upgrade()?;
let git_repo = self.active_repository.as_ref()?;
if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
if let Some(project_path) = project_diff.read(cx).active_path(cx) {
if Some(&entry.repo_path)
== git_repo
.read(cx)
.project_path_to_repo_path(&project_path)
.as_ref()
{
project_diff.focus_handle(cx).focus(window);
return None;
}
}
};
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
})
.ok()
.ok();
self.focus_handle.focus(window);
Some(())
});
}
@@ -1154,6 +1171,17 @@ impl GitPanel {
}
}
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
let message = self.commit_editor.read(cx).text(cx);
if !message.trim().is_empty() {
return Some(message.to_string());
}
self.suggest_commit_message()
.filter(|message| !message.trim().is_empty())
}
pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.clone() else {
return;
@@ -1175,11 +1203,13 @@ impl GitPanel {
return;
}
let mut message = self.commit_editor.read(cx).text(cx);
if message.trim().is_empty() {
let commit_message = self.custom_or_suggested_commit_message(cx);
let Some(mut message) = commit_message else {
self.commit_editor.read(cx).focus_handle(cx).focus(window);
return;
}
};
if self.add_coauthors {
self.fill_co_authors(&mut message, cx);
}
@@ -1310,7 +1340,7 @@ impl GitPanel {
Some("Update")
} else {
None
};
}?;
let file_name = git_status_entry
.repo_path
@@ -1318,17 +1348,17 @@ impl GitPanel {
.unwrap_or_default()
.to_string_lossy();
Some(format!("{} {}", action_text?, file_name))
Some(format!("{} {}", action_text, file_name))
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_commit_message();
let suggested_commit_message = suggested_commit_message
let placeholder_text = suggested_commit_message
.as_deref()
.unwrap_or("Enter commit message");
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
});
cx.notify();
@@ -1665,7 +1695,7 @@ impl GitPanel {
git_panel.commit_editor = cx.new(|cx| {
commit_message_editor(
buffer,
git_panel.suggested_commit_message.as_deref(),
git_panel.suggest_commit_message().as_deref(),
git_panel.project.clone(),
true,
window,
@@ -1898,7 +1928,7 @@ impl GitPanel {
})
}
pub fn can_commit(&self) -> bool {
pub fn can_open_commit_editor(&self) -> bool {
(self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts()
}
@@ -1944,7 +1974,7 @@ impl GitPanel {
}
}
pub fn configure_commit_button(&self, cx: &Context<Self>) -> (bool, &'static str) {
pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
if self.has_unstaged_conflicts() {
(false, "You must resolve conflicts before committing")
} else if !self.has_staged_changes() && !self.has_tracked_changes() {
@@ -1954,19 +1984,20 @@ impl GitPanel {
)
} else if self.pending_commit.is_some() {
(false, "Commit in progress")
} else if self.commit_editor.read(cx).is_empty(cx) {
} else if self.custom_or_suggested_commit_message(cx).is_none() {
(false, "No commit message")
} else if !self.has_write_access(cx) {
(false, "You do not have write access to this project")
} else {
(
true,
if self.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
},
)
(true, self.commit_button_title())
}
}
pub fn commit_button_title(&self) -> &'static str {
if self.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
}
}
@@ -1975,123 +2006,116 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
let active_repository = self.active_repository.clone()?;
let can_open_commit_editor = self.can_open_commit_editor();
let (can_commit, tooltip) = self.configure_commit_button(cx);
let project = self.project.clone().read(cx);
let active_repository = self.active_repository.clone();
let panel_editor_style = panel_editor_style(true, window, cx);
if let Some(active_repo) = active_repository {
let (can_commit, tooltip) = self.configure_commit_button(cx);
let enable_coauthors = self.render_co_authors(cx);
let enable_coauthors = self.render_co_authors(cx);
let title = self.commit_button_title();
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let title = if self.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
};
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let branch = active_repository.read(cx).current_branch().cloned();
let branch = active_repo.read(cx).current_branch()?.clone();
let footer_size = px(32.);
let gap = px(8.0);
let footer_size = px(32.);
let gap = px(8.0);
let max_height = window.line_height() * 5. + gap + footer_size;
let max_height = window.line_height() * 5. + gap + footer_size;
let expand_button_size = px(16.);
let expand_button_size = px(16.);
let git_panel = cx.entity().clone();
let display_name = SharedString::from(Arc::from(
active_repo
.read(cx)
.display_name(project, cx)
.trim_end_matches("/"),
));
let branches = branch_picker::popover(self.project.clone(), window, cx);
let footer = v_flex()
.child(PanelRepoFooter::new(
"footer-button",
display_name,
Some(branch),
Some(git_panel),
Some(branches),
))
.child(
panel_editor_container(window, cx)
.id("commit-editor-container")
.relative()
.h(max_height)
// .w_full()
// .border_t_1()
// .border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.cursor_text()
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx));
}))
.child(
h_flex()
.id("commit-footer")
.absolute()
.bottom_0()
.right_2()
.h(footer_size)
.flex_none()
.children(enable_coauthors)
.child(
panel_filled_button(title)
.tooltip(move |window, cx| {
if can_commit {
Tooltip::for_action_in(
tooltip,
&Commit,
&editor_focus_handle,
window,
cx,
)
} else {
Tooltip::simple(tooltip, cx)
}
let git_panel = cx.entity().clone();
let display_name = SharedString::from(Arc::from(
active_repository
.read(cx)
.display_name(project, cx)
.trim_end_matches("/"),
));
let branches = branch_picker::popover(self.project.clone(), window, cx);
let footer = v_flex()
.child(PanelRepoFooter::new(
"footer-button",
display_name,
branch,
Some(git_panel),
Some(branches),
))
.child(
panel_editor_container(window, cx)
.id("commit-editor-container")
.relative()
.h(max_height)
// .w_full()
// .border_t_1()
// .border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.cursor_text()
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx));
}))
.child(
h_flex()
.id("commit-footer")
.absolute()
.bottom_0()
.right_2()
.h(footer_size)
.flex_none()
.children(enable_coauthors)
.child(
panel_filled_button(title)
.tooltip(move |window, cx| {
if can_commit {
Tooltip::for_action_in(
tooltip,
&Commit,
&editor_focus_handle,
window,
cx,
)
} else {
Tooltip::simple(tooltip, cx)
}
})
.disabled(!can_commit || self.modal_open)
.on_click({
cx.listener(move |this, _: &ClickEvent, window, cx| {
this.commit_changes(window, cx)
})
.disabled(!can_commit || self.modal_open)
.on_click({
cx.listener(move |this, _: &ClickEvent, window, cx| {
this.commit_changes(window, cx)
})
}),
),
)
// .when(!self.modal_open, |el| {
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
.child(
div()
.absolute()
.top_1()
.right_2()
.opacity(0.5)
.hover(|this| this.opacity(1.0))
.w(expand_button_size)
.child(
panel_icon_button("expand-commit-editor", IconName::Maximize)
.icon_size(IconSize::Small)
.style(ButtonStyle::Transparent)
.width(expand_button_size.into())
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
git::ShowCommitEditor.boxed_clone(),
cx,
)
}
})),
),
),
);
}),
),
)
// .when(!self.modal_open, |el| {
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
.child(
div()
.absolute()
.top_1()
.right_2()
.opacity(0.5)
.hover(|this| this.opacity(1.0))
.w(expand_button_size)
.child(
panel_icon_button("expand-commit-editor", IconName::Maximize)
.icon_size(IconSize::Small)
.style(ButtonStyle::Transparent)
.width(expand_button_size.into())
.disabled(!can_open_commit_editor)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
git::ShowCommitEditor.boxed_clone(),
cx,
)
}
})),
),
),
);
Some(footer)
} else {
None
}
Some(footer)
}
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
@@ -2118,7 +2142,7 @@ impl GitPanel {
.child(
Label::new(commit.subject.clone())
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.id("commit-msg-hover")
.hoverable_tooltip(move |window, cx| {
@@ -2274,7 +2298,7 @@ impl GitPanel {
) -> impl IntoElement {
let entry_count = self.entries.len();
v_flex()
h_flex()
.size_full()
.flex_grow()
.overflow_hidden()
@@ -2444,7 +2468,7 @@ impl GitPanel {
ix: usize,
entry: &GitStatusEntry,
has_write_access: bool,
_: &Window,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let display_name = entry
@@ -2536,6 +2560,10 @@ impl GitPanel {
.h(self.list_item_height())
.w_full()
.items_center()
.border_1()
.when(selected && self.focus_handle.is_focused(window), |el| {
el.border_color(cx.theme().colors().border_focused)
})
.px(rems(0.75)) // ~12px
.overflow_hidden()
.flex_none()
@@ -2551,6 +2579,7 @@ impl GitPanel {
this.open_file(&Default::default(), window, cx)
} else {
this.open_diff(&Default::default(), window, cx);
this.focus_handle.focus(window);
}
})
})
@@ -3296,41 +3325,32 @@ impl RenderOnce for PanelRepoFooter {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let active_repo = self.active_repository.clone();
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
let repo_selector_trigger = Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted);
let repo_selector = if let Some(panel) = self.git_panel.clone() {
let repo_selector = panel.read(cx).repository_selector.clone();
let repo_count = repo_selector.read(cx).repositories_len(cx);
if repo_count > 1 {
RepositorySelectorPopoverMenu::new(
panel.read(cx).repository_selector.clone(),
Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted),
Tooltip::text("Choose a repository"),
)
.into_any_element()
} else {
Label::new(active_repo)
.size(LabelSize::Small)
.color(Color::Muted)
.line_height_style(LineHeightStyle::UiLabel)
.into_any_element()
}
let single_repo = repo_count == 1;
RepositorySelectorPopoverMenu::new(
panel.read(cx).repository_selector.clone(),
repo_selector_trigger.disabled(single_repo).truncate(true),
Tooltip::text("Switch active repository"),
)
.into_any_element()
} else {
Button::new("repo-selector", active_repo.clone())
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
// for rendering preview, we don't have git_panel there
repo_selector_trigger.into_any_element()
};
let branch = self.branch.clone();
let branch_name = branch
.as_ref()
.map_or("<no branch>".into(), |branch| branch.name.clone());
.map_or(" (no branch)".into(), |branch| branch.name.clone());
let branches = self.branches.clone();
@@ -3338,6 +3358,7 @@ impl RenderOnce for PanelRepoFooter {
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.truncate(true)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
&zed_actions::git::Branch,
@@ -3372,36 +3393,31 @@ impl RenderOnce for PanelRepoFooter {
.justify_between()
.child(
h_flex()
.relative()
.flex_1()
.overflow_hidden()
.items_center()
.gap_0p5()
.child(
div()
// .when(repo_or_branch_has_uppercase, |this| {
// this.relative().pt(px(2.))
// })
.child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
),
div().child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.child(
h_flex()
.gap_0p5()
.child(repo_selector)
.child(
div()
.text_color(cx.theme().colors().text_muted)
.text_sm()
.child("/"),
)
.child(branch_selector),
),
.child(repo_selector)
.when_some(branch.clone(), |this, _| {
this.child(
div()
.text_color(cx.theme().colors().text_muted)
.text_sm()
.child("/"),
)
})
.child(branch_selector),
)
.child(
h_flex()
.gap_1()
.flex_shrink_0()
.children(spinner)
.child(self.render_overflow_menu(overflow_menu_id))
.when_some(branch, |this, branch| {
@@ -3462,94 +3478,220 @@ impl ComponentPreview for PanelRepoFooter {
}
}
fn custom(branch_name: &str, upstream: Option<UpstreamTracking>) -> Branch {
Branch {
is_head: true,
name: branch_name.to_string().into(),
upstream: upstream.map(|tracking| Upstream {
ref_name: format!("zed/{}", branch_name).into(),
tracking,
}),
most_recent_commit: Some(CommitSummary {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
}),
}
}
fn active_repository(id: usize) -> SharedString {
format!("repo-{}", id).into()
}
let example_width = px(340.);
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"no-branch",
active_repository(1).clone(),
None,
))
.into_any_element(),
),
)
.grow(),
single_example(
"Remote status unknown",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"unknown-upstream",
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"No Remote Upstream",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"no-remote-upstream",
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Not Ahead or Behind",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"not-ahead-or-behind",
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Behind remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"behind-remote",
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Ahead of remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"ahead-of-remote",
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Ahead and behind remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"ahead-and-behind",
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
],
)
.grow()
.vertical()])
.children(vec![example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"short-branch",
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-branch",
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-repo",
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-repo-and-branch",
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"uppercase-repo",
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"uppercase-branch",
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical()])
.into_any_element()
}

View File

@@ -5,7 +5,7 @@ use collections::HashSet;
use editor::{
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
Editor, EditorEvent, ToPoint,
Editor, EditorEvent,
};
use feature_flags::FeatureFlagViewExt;
use futures::StreamExt;
@@ -192,6 +192,19 @@ impl ProjectDiff {
self.move_to_path(path_key, window, cx)
}
pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {
let editor = self.editor.read(cx);
let position = editor.selections.newest_anchor().head();
let multi_buffer = editor.buffer().read(cx);
let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?;
let file = buffer.read(cx).file()?;
Some(ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
})
}
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
self.editor.update(cx, |editor, cx| {
@@ -244,14 +257,14 @@ impl ProjectDiff {
}
}
}
let mut commit = false;
let mut can_open_commit_editor = false;
let mut stage_all = false;
let mut unstage_all = false;
self.workspace
.read_with(cx, |workspace, cx| {
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
let git_panel = git_panel.read(cx);
commit = git_panel.can_commit();
can_open_commit_editor = git_panel.can_open_commit_editor();
stage_all = git_panel.can_stage_all();
unstage_all = git_panel.can_unstage_all();
}
@@ -263,7 +276,7 @@ impl ProjectDiff {
unstage: has_staged_hunks,
prev_next,
selection,
commit,
can_open_commit_editor,
stage_all,
unstage_all,
};
@@ -271,41 +284,26 @@ impl ProjectDiff {
fn handle_editor_event(
&mut self,
editor: &Entity<Editor>,
_: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| {
let anchor = editor.scroll_manager.anchor().anchor;
let multibuffer = self.multibuffer.read(cx);
let snapshot = multibuffer.snapshot(cx);
let mut point = anchor.to_point(&snapshot);
point.row = (point.row + 1).min(snapshot.max_row().0);
point.column = 0;
let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(point, cx)
else {
return;
};
let Some(project_path) = buffer
.read(cx)
.file()
.map(|file| (file.worktree_id(cx), file.path().clone()))
else {
EditorEvent::SelectionsChanged { local: true } => {
let Some(project_path) = self.active_path(cx) else {
return;
};
self.workspace
.update(cx, |workspace, cx| {
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
git_panel.update(cx, |git_panel, cx| {
git_panel.select_entry_by_path(project_path.into(), window, cx)
git_panel.select_entry_by_path(project_path, window, cx)
})
}
})
.ok();
}),
}
_ => {}
}
}
@@ -400,6 +398,7 @@ impl ProjectDiff {
self.editor.update(cx, |editor, cx| {
if was_empty {
editor.change_selections(None, window, cx, |selections| {
// TODO select the very beginning (possibly inside a deletion)
selections.select_ranges([0..0])
});
}
@@ -774,7 +773,7 @@ struct ButtonStates {
selection: bool,
stage_all: bool,
unstage_all: bool,
commit: bool,
can_open_commit_editor: bool,
}
impl Render for ProjectDiffToolbar {
@@ -813,10 +812,8 @@ impl Render for ProjectDiffToolbar {
el.child(
Button::new("stage", "Stage")
.tooltip(Tooltip::for_action_title_in(
"Stage",
&StageAndNext {
whole_excerpt: false,
},
"Stage and go to next hunk",
&StageAndNext,
&focus_handle,
))
// don't actually disable the button so it's mashable
@@ -826,22 +823,14 @@ impl Render for ProjectDiffToolbar {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(
&StageAndNext {
whole_excerpt: false,
},
window,
cx,
)
this.dispatch_action(&StageAndNext, window, cx)
})),
)
.child(
Button::new("unstage", "Unstage")
.tooltip(Tooltip::for_action_title_in(
"Unstage",
&UnstageAndNext {
whole_excerpt: false,
},
"Unstage and go to next hunk",
&UnstageAndNext,
&focus_handle,
))
.color(if button_states.unstage {
@@ -850,13 +839,7 @@ impl Render for ProjectDiffToolbar {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(
&UnstageAndNext {
whole_excerpt: false,
},
window,
cx,
)
this.dispatch_action(&UnstageAndNext, window, cx)
})),
)
}),
@@ -870,20 +853,12 @@ impl Render for ProjectDiffToolbar {
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::for_action_title_in(
"Go to previous hunk",
&GoToPreviousHunk {
center_cursor: false,
},
&GoToPreviousHunk,
&focus_handle,
))
.disabled(!button_states.prev_next)
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(
&GoToPreviousHunk {
center_cursor: true,
},
window,
cx,
)
this.dispatch_action(&GoToPreviousHunk, window, cx)
})),
)
.child(
@@ -891,20 +866,12 @@ impl Render for ProjectDiffToolbar {
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::for_action_title_in(
"Go to next hunk",
&GoToHunk {
center_cursor: false,
},
&GoToHunk,
&focus_handle,
))
.disabled(!button_states.prev_next)
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(
&GoToHunk {
center_cursor: true,
},
window,
cx,
)
this.dispatch_action(&GoToHunk, window, cx)
})),
),
)
@@ -950,7 +917,7 @@ impl Render for ProjectDiffToolbar {
)
.child(
Button::new("commit", "Commit")
.disabled(!button_states.commit)
.disabled(!button_states.can_open_commit_editor)
.tooltip(Tooltip::for_action_title_in(
"Commit",
&ShowCommitEditor,
@@ -980,6 +947,11 @@ mod tests {
use super::*;
#[ctor::ctor]
fn init_logger() {
env_logger::init();
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
@@ -1152,4 +1124,107 @@ mod tests {
.unindent(),
);
}
#[gpui::test]
async fn test_hunks_after_restore_then_modify(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"foo": "modified\n",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/project/foo"), cx)
})
.await
.unwrap();
let buffer_editor = cx.new_window_entity(|window, cx| {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
});
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
});
cx.run_until_parked();
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("foo".into(), "original\n".into())],
);
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.statuses = HashMap::from_iter([(
"foo".into(),
TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Modified,
}
.into(),
)]);
});
cx.run_until_parked();
let diff_editor = diff.update(cx, |diff, _| diff.editor.clone());
assert_state_with_diff(
&diff_editor,
cx,
&"
- original
+ ˇmodified
"
.unindent(),
);
let prev_buffer_hunks =
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
let snapshot = buffer_editor.snapshot(window, cx);
let snapshot = &snapshot.buffer_snapshot;
let prev_buffer_hunks = buffer_editor
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
.collect::<Vec<_>>();
buffer_editor.git_restore(&Default::default(), window, cx);
prev_buffer_hunks
});
assert_eq!(prev_buffer_hunks.len(), 1);
cx.run_until_parked();
let new_buffer_hunks =
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
let snapshot = buffer_editor.snapshot(window, cx);
let snapshot = &snapshot.buffer_snapshot;
let new_buffer_hunks = buffer_editor
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
.collect::<Vec<_>>();
buffer_editor.git_restore(&Default::default(), window, cx);
new_buffer_hunks
});
assert_eq!(new_buffer_hunks.as_slice(), &[]);
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
buffer_editor.set_text("different\n", window, cx);
buffer_editor.save(false, project.clone(), window, cx)
})
.await
.unwrap();
cx.run_until_parked();
assert_state_with_diff(
&diff_editor,
cx,
&"
- original
+ ˇdifferent
"
.unindent(),
);
}
}

View File

@@ -71,7 +71,7 @@ impl RemoteOutputToast {
}
});
let message;
let mut message: SharedString;
let remote;
match action {
@@ -86,19 +86,32 @@ impl RemoteOutputToast {
RemoteAction::Push(remote_ref) => {
message = output.stdout.trim().to_string().into();
let remote_message = get_remote_lines(&output.stderr);
let finder = LinkFinder::new();
let links = finder
.links(&remote_message)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.collect_vec();
if message.is_empty() {
message = output.stderr.trim().to_string().into();
if message.is_empty() {
message = "Push Successful".into();
}
remote = None;
} else {
let remote_message = get_remote_lines(&output.stderr);
remote = Some(InfoFromRemote {
name: remote_ref.name,
remote_text: remote_message.into(),
links,
});
remote = if remote_message.is_empty() {
None
} else {
let finder = LinkFinder::new();
let links = finder
.links(&remote_message)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.collect_vec();
Some(InfoFromRemote {
name: remote_ref.name,
remote_text: remote_message.into(),
links,
})
}
}
}
}

View File

@@ -670,6 +670,14 @@ pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
}
}
/// Creates a solid background color.
pub fn solid_background(color: impl Into<Hsla>) -> Background {
Background {
solid: color.into(),
..Default::default()
}
}
/// Creates a LinearGradient background color.
///
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.

View File

@@ -401,9 +401,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.pl_0p5()
.w(px(240.))
.child(
div().max_w_40().child(
Label::new(model_info.model.name().0.clone()).text_ellipsis(),
),
div()
.max_w_40()
.child(Label::new(model_info.model.name().0.clone()).truncate()),
)
.child(
h_flex()

View File

@@ -2062,7 +2062,12 @@ impl MultiBuffer {
if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
buffer_state.excerpts.retain(|l| l != &excerpt.locator);
if buffer_state.excerpts.is_empty() {
log::debug!(
"removing buffer and diff for buffer {}",
excerpt.buffer_id
);
buffers.remove(&excerpt.buffer_id);
self.diffs.remove(&excerpt.buffer_id);
}
}
cursor.next(&());
@@ -2716,6 +2721,11 @@ impl MultiBuffer {
snapshot.has_deleted_file = has_deleted_file;
snapshot.has_conflict = has_conflict;
snapshot.diffs.retain(|_, _| false);
for (id, diff) in self.diffs.iter() {
snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx));
}
excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
let mut edits = Vec::new();
@@ -3476,7 +3486,10 @@ impl MultiBufferSnapshot {
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
let query_range = range.start.to_point(self)..range.end.to_point(self);
self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| {
let diff = self.diffs.get(&buffer.remote_id())?;
let Some(diff) = self.diffs.get(&buffer.remote_id()) else {
log::debug!("no diff found for {:?}", buffer.remote_id());
return None;
};
let buffer_start = buffer.anchor_before(buffer_range.start);
let buffer_end = buffer.anchor_after(buffer_range.end);
Some(
@@ -3485,17 +3498,12 @@ impl MultiBufferSnapshot {
if hunk.is_created_file() && !self.all_diff_hunks_expanded {
return None;
}
Some((
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
hunk,
))
Some((hunk.range.clone(), hunk))
}),
)
})
.filter_map(move |(range, hunk, excerpt)| {
if range.start != range.end
&& range.end == query_range.start
&& !hunk.row_range.is_empty()
if range.start != range.end && range.end == query_range.start && !hunk.range.is_empty()
{
return None;
}
@@ -3828,12 +3836,12 @@ impl MultiBufferSnapshot {
&excerpt.buffer,
) {
let hunk_range = hunk.buffer_range.to_offset(&excerpt.buffer);
if hunk.row_range.end >= buffer_end_row {
if hunk.range.end >= Point::new(buffer_end_row, 0) {
continue;
}
let hunk_start = Point::new(hunk.row_range.start, 0);
let hunk_end = Point::new(hunk.row_range.end, 0);
let hunk_start = hunk.range.start;
let hunk_end = hunk.range.end;
cursor.seek_to_buffer_position_in_current_excerpt(&DimensionPair {
key: hunk_range.start,

View File

@@ -2160,6 +2160,7 @@ impl ReferenceMultibuffer {
.unwrap();
let excerpt = self.excerpts.remove(ix);
let buffer = excerpt.buffer.read(cx);
let id = buffer.remote_id();
log::info!(
"Removing excerpt {}: {:?}",
ix,
@@ -2167,6 +2168,13 @@ impl ReferenceMultibuffer {
.text_for_range(excerpt.range.to_offset(buffer))
.collect::<String>(),
);
if !self
.excerpts
.iter()
.any(|excerpt| excerpt.buffer.read(cx).remote_id() == id)
{
self.diffs.remove(&id);
}
}
fn insert_excerpt_after(
@@ -2266,7 +2274,7 @@ impl ReferenceMultibuffer {
}
if !hunk.buffer_range.start.is_valid(&buffer) {
log::trace!("skipping hunk with deleted start: {:?}", hunk.row_range);
log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
continue;
}
@@ -2415,6 +2423,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
.unwrap_or(10);
let mut buffers: Vec<Entity<Buffer>> = Vec::new();
let mut base_texts: HashMap<BufferId, String> = HashMap::default();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let mut reference = ReferenceMultibuffer::default();
let mut anchors = Vec::new();
@@ -2522,9 +2531,10 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap();
log::info!(
"expanding diff hunks in range {:?} (excerpt id {:?}) index {excerpt_ix:?})",
"expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})",
range.to_offset(&snapshot),
excerpt.id
excerpt.id,
excerpt.buffer.read(cx).remote_id(),
);
reference.expand_diff_hunks(excerpt.id, start..end, cx);
multibuffer.expand_diff_hunks(vec![range], cx);
@@ -2534,7 +2544,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
multibuffer.update(cx, |multibuffer, cx| {
for buffer in multibuffer.all_buffers() {
let snapshot = buffer.read(cx).snapshot();
let _ = multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
cx,
|diff, cx| {
log::info!(
@@ -2551,17 +2561,16 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
}
_ => {
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
let base_text = util::RandomCharIter::new(&mut rng)
let mut base_text = util::RandomCharIter::new(&mut rng)
.take(256)
.collect::<String>();
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
multibuffer.update(cx, |multibuffer, cx| {
reference.add_diff(diff.clone(), cx);
multibuffer.add_diff(diff, cx)
});
text::LineEnding::normalize(&mut base_text);
base_texts.insert(
buffer.read_with(cx, |buffer, _| buffer.remote_id()),
base_text,
);
buffers.push(buffer);
buffers.last().unwrap()
} else {
@@ -2595,6 +2604,18 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
(start_ix..end_ix, anchor_range)
});
multibuffer.update(cx, |multibuffer, cx| {
let id = buffer_handle.read(cx).remote_id();
if multibuffer.diff_for(id).is_none() {
let base_text = base_texts.get(&id).unwrap();
let diff = cx.new(|cx| {
BufferDiff::new_with_base_text(base_text, &buffer_handle, cx)
});
reference.add_diff(diff.clone(), cx);
multibuffer.add_diff(diff, cx)
}
});
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
multibuffer
.insert_excerpts_after(

View File

@@ -136,6 +136,15 @@ impl BufferDiffState {
let _ = self.diff_bases_changed(buffer, diff_bases_change, cx);
}
pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
if self.diff_updated_futures.is_empty() {
return None;
}
let (tx, rx) = oneshot::channel();
self.diff_updated_futures.push(tx);
Some(rx)
}
fn diff_bases_changed(
&mut self,
buffer: text::BufferSnapshot,
@@ -1362,8 +1371,23 @@ impl BufferStore {
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferDiff>>> {
let buffer_id = buffer.read(cx).remote_id();
if let Some(diff) = self.get_unstaged_diff(buffer_id, cx) {
return Task::ready(Ok(diff));
if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
if let Some(unstaged_diff) = diff_state
.read(cx)
.unstaged_diff
.as_ref()
.and_then(|weak| weak.upgrade())
{
if let Some(task) =
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
{
return cx.background_executor().spawn(async move {
task.await?;
Ok(unstaged_diff)
});
}
return Task::ready(Ok(unstaged_diff));
}
}
let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
@@ -1402,8 +1426,24 @@ impl BufferStore {
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferDiff>>> {
let buffer_id = buffer.read(cx).remote_id();
if let Some(diff) = self.get_uncommitted_diff(buffer_id, cx) {
return Task::ready(Ok(diff));
if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
if let Some(uncommitted_diff) = diff_state
.read(cx)
.uncommitted_diff
.as_ref()
.and_then(|weak| weak.upgrade())
{
if let Some(task) =
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
{
return cx.background_executor().spawn(async move {
task.await?;
Ok(uncommitted_diff)
});
}
return Task::ready(Ok(uncommitted_diff));
}
}
let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {

View File

@@ -6,10 +6,7 @@ use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
use git::repository::{GitRepository, RepoPath};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
WeakEntity,
@@ -24,7 +21,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use text::BufferId;
use util::{maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
pub struct GitStore {
buffer_store: Entity<BufferStore>,
@@ -691,6 +688,33 @@ impl Repository {
self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
}
// note: callers must verify these come from the same worktree
pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
let other_work_dir = &other.read(cx).repository_entry.work_directory;
match (&self.repository_entry.work_directory, other_work_dir) {
(WorkDirectory::InProject { .. }, WorkDirectory::AboveProject { .. }) => false,
(WorkDirectory::AboveProject { .. }, WorkDirectory::InProject { .. }) => true,
(
WorkDirectory::InProject {
relative_path: this_path,
},
WorkDirectory::InProject {
relative_path: other_path,
},
) => other_path.starts_with(this_path),
(
WorkDirectory::AboveProject {
absolute_path: this_path,
..
},
WorkDirectory::AboveProject {
absolute_path: other_path,
..
},
) => other_path.starts_with(this_path),
}
}
pub fn worktree_id_path_to_repo_path(
&self,
worktree_id: WorktreeId,
@@ -1046,18 +1070,6 @@ impl Repository {
self.repository_entry.status_len()
}
fn have_changes(&self) -> bool {
self.repository_entry.status_summary() != GitSummary::UNCHANGED
}
fn have_staged_changes(&self) -> bool {
self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
}
pub fn can_commit(&self, commit_all: bool) -> bool {
return self.have_changes() && (commit_all || self.have_staged_changes());
}
pub fn commit(
&self,
message: SharedString,

View File

@@ -4310,16 +4310,26 @@ impl Project {
.buffer_for_id(buffer_id, cx)?
.read(cx)
.project_path(cx)?;
self.git_store
.read(cx)
.all_repositories()
.into_iter()
.find_map(|repo| {
Some((
repo.clone(),
repo.read(cx).repository_entry.relativize(&path.path).ok()?,
))
})
let mut found: Option<(Entity<Repository>, RepoPath)> = None;
for repo_handle in self.git_store.read(cx).all_repositories() {
let repo = repo_handle.read(cx);
if repo.worktree_id != path.worktree_id {
continue;
}
let Ok(relative_path) = repo.repository_entry.relativize(&path.path) else {
continue;
};
if found
.as_ref()
.is_some_and(|(found, _)| repo.contains_sub_repo(found, cx))
{
continue;
}
found = Some((repo_handle.clone(), relative_path))
}
found
}
}

View File

@@ -168,6 +168,10 @@ pub struct GitSettings {
///
/// Default: on
pub inline_blame: Option<InlineBlameSettings>,
/// How hunks are displayed visually in the editor.
///
/// Default: transparent
pub hunk_style: Option<GitHunkStyleSetting>,
}
impl GitSettings {
@@ -200,6 +204,16 @@ impl GitSettings {
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitHunkStyleSetting {
/// Show unstaged hunks with a transparent background
#[default]
Transparent,
/// Show unstaged hunks with a pattern background
Pattern,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitGutterSetting {

View File

@@ -401,7 +401,7 @@ impl TitleBar {
.child(
Label::new(nickname.clone())
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
),
)
.tooltip(move |window, cx| {

View File

@@ -97,6 +97,7 @@ pub struct Button {
key_binding: Option<KeyBinding>,
key_binding_position: KeybindingPosition,
alpha: Option<f32>,
truncate: bool,
}
impl Button {
@@ -123,6 +124,7 @@ impl Button {
key_binding: None,
key_binding_position: KeybindingPosition::default(),
alpha: None,
truncate: false,
}
}
@@ -206,6 +208,15 @@ impl Button {
self.alpha = Some(alpha);
self
}
/// Truncates overflowing labels with an ellipsis (`…`) if needed.
///
/// Buttons with static labels should _never_ be truncated, ensure
/// this is only used when the label is dynamic and may overflow.
pub fn truncate(mut self, truncate: bool) -> Self {
self.truncate = truncate;
self
}
}
impl Toggleable for Button {
@@ -437,7 +448,8 @@ impl RenderOnce for Button {
.color(label_color)
.size(self.label_size.unwrap_or_default())
.when_some(self.alpha, |this, alpha| this.alpha(alpha))
.line_height_style(LineHeightStyle::UiLabel),
.line_height_style(LineHeightStyle::UiLabel)
.when(self.truncate, |this| this.truncate()),
)
.children(self.key_binding),
)

View File

@@ -64,8 +64,8 @@ impl LabelCommon for HighlightedLabel {
self
}
fn text_ellipsis(mut self) -> Self {
self.base = self.base.text_ellipsis();
fn truncate(mut self) -> Self {
self.base = self.base.truncate();
self
}

View File

@@ -171,8 +171,9 @@ impl LabelCommon for Label {
self
}
fn text_ellipsis(mut self) -> Self {
self.base = self.base.text_ellipsis();
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn truncate(mut self) -> Self {
self.base = self.base.truncate();
self
}
@@ -240,7 +241,7 @@ mod label_preview {
"Special Cases",
vec![
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").text_ellipsis()).into_any_element()),
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
],
),
])

View File

@@ -57,7 +57,7 @@ pub trait LabelCommon {
fn alpha(self, alpha: f32) -> Self;
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn text_ellipsis(self) -> Self;
fn truncate(self) -> Self;
/// Sets the label to render as a single line.
fn single_line(self) -> Self;
@@ -84,7 +84,7 @@ pub struct LabelLike {
alpha: Option<f32>,
underline: bool,
single_line: bool,
text_ellipsis: bool,
truncate: bool,
}
impl Default for LabelLike {
@@ -109,7 +109,7 @@ impl LabelLike {
alpha: None,
underline: false,
single_line: false,
text_ellipsis: false,
truncate: false,
}
}
}
@@ -166,8 +166,9 @@ impl LabelCommon for LabelLike {
self
}
fn text_ellipsis(mut self) -> Self {
self.text_ellipsis = true;
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn truncate(mut self) -> Self {
self.truncate = true;
self
}
@@ -220,7 +221,7 @@ impl RenderOnce for LabelLike {
})
.when(self.strikethrough, |this| this.line_through())
.when(self.single_line, |this| this.whitespace_nowrap())
.when(self.text_ellipsis, |this| {
.when(self.truncate, |this| {
this.overflow_x_hidden().text_ellipsis()
})
.text_color(color)

View File

@@ -55,6 +55,7 @@ git_ui.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
project_panel.workspace = true

View File

@@ -1329,12 +1329,25 @@ pub(crate) fn start_of_relative_buffer_row(
fn up_down_buffer_rows(
map: &DisplaySnapshot,
point: DisplayPoint,
mut point: DisplayPoint,
mut goal: SelectionGoal,
times: isize,
mut times: isize,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let bias = if times < 0 { Bias::Left } else { Bias::Right };
while map.is_folded_buffer_header(point.row()) {
if times < 0 {
(point, _) = movement::up(map, point, goal, true, text_layout_details);
times += 1;
} else if times > 0 {
(point, _) = movement::down(map, point, goal, true, text_layout_details);
times -= 1;
} else {
break;
}
}
let start = map.display_point_to_fold_point(point, Bias::Left);
let begin_folded_line = map.fold_point_to_display_point(
map.fold_snapshot

View File

@@ -6,9 +6,13 @@ use std::time::Duration;
use collections::HashMap;
use command_palette::CommandPalette;
use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint};
use editor::{
actions::DeleteLine, display_map::DisplayRow, test::editor_test_context::EditorTestContext,
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use futures::StreamExt;
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
use language::Point;
pub use neovim_backed_test_context::*;
use settings::SettingsStore;
pub use vim_test_context::*;
@@ -1707,3 +1711,202 @@ async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) {
cx.simulate_shared_keystrokes("l l escape .").await;
cx.shared_state().await.assert_eq("hellˇllo world.");
}
#[gpui::test]
async fn test_folded_multibuffer_excerpts(cx: &mut gpui::TestAppContext) {
VimTestContext::init(cx);
cx.update(|cx| {
VimTestContext::init_keybindings(true, cx);
});
let (editor, cx) = cx.add_window_view(|window, cx| {
let multi_buffer = MultiBuffer::build_multi(
[
("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
("aaa\nbbb\nccc\nddd\n", vec![Point::row_range(0..2)]),
("AAA\nBBB\nCCC\nDDD\n", vec![Point::row_range(0..2)]),
("one\ntwo\nthr\nfou\n", vec![Point::row_range(0..2)]),
],
cx,
);
let mut editor = Editor::new(
EditorMode::Full,
multi_buffer.clone(),
None,
true,
window,
cx,
);
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
// fold all but the second buffer, so that we test navigating between two
// adjacent folded buffers, as well as folded buffers at the start and
// end the multibuffer
editor.fold_buffer(buffer_ids[0], cx);
editor.fold_buffer(buffer_ids[2], cx);
editor.fold_buffer(buffer_ids[3], cx);
editor
});
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("j");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇaaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("j");
cx.simulate_keystroke("j");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
aaa
bbb
ˇ[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("j");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("j");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇ[FOLDED]
"
});
cx.simulate_keystroke("k");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("k");
cx.simulate_keystroke("k");
cx.simulate_keystroke("k");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇaaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("k");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("shift-g");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇ[FOLDED]
"
});
cx.simulate_keystrokes("g g");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
aaa
bbb
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.update_editor(|editor, _, cx| {
let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
editor.fold_buffer(buffer_ids[1], cx);
});
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystrokes("2 j");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
"
});
}

View File

@@ -24,6 +24,10 @@ impl VimTestContext {
git_ui::init(cx);
crate::init(cx);
search::init(cx);
language::init(cx);
editor::init_settings(cx);
project::Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
});
}
@@ -56,22 +60,26 @@ impl VimTestContext {
)
}
pub fn init_keybindings(enabled: bool, cx: &mut App) {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
});
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
"keymaps/default-macos.json",
cx,
)
.unwrap();
cx.bind_keys(default_key_bindings);
if enabled {
let vim_key_bindings =
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
cx.bind_keys(vim_key_bindings);
}
}
pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
cx.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
});
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
"keymaps/default-macos.json",
cx,
)
.unwrap();
cx.bind_keys(default_key_bindings);
if enabled {
let vim_key_bindings =
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
cx.bind_keys(vim_key_bindings);
}
Self::init_keybindings(enabled, cx);
});
// Setup search toolbars and keypress hook

View File

@@ -4292,7 +4292,11 @@ impl BackgroundScanner {
let mut containing_git_repository = None;
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
if index != 0 {
if let Ok(ignore) =
if Some(ancestor) == self.fs.home_dir().as_deref() {
// Unless $HOME is itself the worktree root, don't consider it as a
// containing git repository---expensive and likely unwanted.
break;
} else if let Ok(ignore) =
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
{
self.state
@@ -4304,6 +4308,7 @@ impl BackgroundScanner {
}
let ancestor_dot_git = ancestor.join(*DOT_GIT);
log::debug!("considering ancestor: {ancestor_dot_git:?}");
// Check whether the directory or file called `.git` exists (in the
// case of worktrees it's a file.)
if self
@@ -4312,21 +4317,26 @@ impl BackgroundScanner {
.await
.is_ok_and(|metadata| metadata.is_some())
{
log::debug!(".git path exists");
if index != 0 {
// We canonicalize, since the FS events use the canonicalized path.
if let Some(ancestor_dot_git) =
self.fs.canonicalize(&ancestor_dot_git).await.log_err()
{
let location_in_repo = root_abs_path
.as_path()
.strip_prefix(ancestor)
.unwrap()
.into();
log::debug!(
"inserting parent git repo for this worktree: {location_in_repo:?}"
);
// We associate the external git repo with our root folder and
// also mark where in the git repo the root folder is located.
let local_repository = self.state.lock().insert_git_repository_for_path(
WorkDirectory::AboveProject {
absolute_path: ancestor.into(),
location_in_repo: root_abs_path
.as_path()
.strip_prefix(ancestor)
.unwrap()
.into(),
location_in_repo,
},
ancestor_dot_git.clone().into(),
self.fs.as_ref(),
@@ -4341,9 +4351,13 @@ impl BackgroundScanner {
// Reached root of git repository.
break;
} else {
log::debug!(".git path doesn't exist");
}
}
log::debug!("containing git repository: {containing_git_repository:?}");
let (scan_job_tx, scan_job_rx) = channel::unbounded();
{
let mut state = self.state.lock();

View File

@@ -2241,6 +2241,73 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_home_dir_as_git_repository(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root",
json!({
"home": {
".git": {},
"project": {
"a.txt": "A"
},
},
}),
)
.await;
fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
let tree = Worktree::local(
Path::new(path!("/root/home/project")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
let repo = tree.repository_for_path(path!("a.txt").as_ref());
assert!(repo.is_none());
});
let home_tree = Worktree::local(
Path::new(path!("/root/home")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| home_tree.read(cx).as_local().unwrap().scan_complete())
.await;
home_tree.flush_fs_events(cx).await;
home_tree.read_with(cx, |home_tree, _cx| {
let home_tree = home_tree.as_local().unwrap();
let repo = home_tree.repository_for_path(path!("project/a.txt").as_ref());
assert_eq!(
repo.map(|repo| &repo.work_directory),
Some(&WorkDirectory::InProject {
relative_path: Path::new("").into()
})
);
})
}
#[gpui::test]
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.177.0"
version = "0.177.1"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -1020,7 +1020,7 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) {
let extension_store = ExtensionStore::global(cx);
let theme_registry = ThemeRegistry::global(cx);
let theme_settings = ThemeSettings::get_global(cx);
let appearance = cx.window_appearance().into();
let appearance = SystemAppearance::global(cx).0;
if let Some(theme_selection) = theme_settings.theme_selection.as_ref() {
let theme_name = theme_selection.theme(appearance);

View File

@@ -182,18 +182,8 @@ impl Render for QuickActionBar {
.action("Next Problem", Box::new(GoToDiagnostic))
.action("Previous Problem", Box::new(GoToPreviousDiagnostic))
.separator()
.action(
"Next Hunk",
Box::new(GoToHunk {
center_cursor: true,
}),
)
.action(
"Previous Hunk",
Box::new(GoToPreviousHunk {
center_cursor: true,
}),
)
.action("Next Hunk", Box::new(GoToHunk))
.action("Previous Hunk", Box::new(GoToPreviousHunk))
.separator()
.action("Move Line Up", Box::new(MoveLineUp))
.action("Move Line Down", Box::new(MoveLineDown))

View File

@@ -10,6 +10,12 @@ To preview the docs locally you will need to install [mdBook](https://rust-lang.
mdbook serve docs
```
Before committing, verify that the docs are formatted in the way prettier expects with:
```
cd docs && pnpm dlx prettier@3.5.0 . --write && cd ..
```
## Preprocessor
We have a custom mdbook preprocessor for interfacing with our crates (`crates/docs_preprocessor`).

View File

@@ -72,10 +72,15 @@ The following commands use the language server to help you navigate and refactor
### Git
| Command | Default Shortcut |
| ------------------------- | ---------------- |
| Go to next git change | `] c` |
| Go to previous git change | `[ c` |
| Command | Default Shortcut |
| ------------------------------- | ---------------- |
| Go to next git change | `] c` |
| Go to previous git change | `[ c` |
| Expand diff hunk | `d o` |
| Toggle staged | `d O` |
| Stage and next (in diff view) | `d u` |
| Unstage and next (in diff view) | `d U` |
| Restore change | `d p` |
### Treesitter