Compare commits

...

46 Commits

Author SHA1 Message Date
Joseph T. Lyons
504df9cc27 v0.178.x stable 2025-03-19 11:32:34 -04:00
Joseph T. Lyons
7816e05135 Clean up community_release_actions file (#27027)
Release Notes:

- N/A
2025-03-18 17:30:18 -04:00
Joseph T. Lyons
b5095c3e51 zed 0.178.4 2025-03-18 13:57:06 -04:00
Cole Miller
1702cf7623 worktree: Fix tracking of git status scans and re-enable tests (#26926)
Closes #ISSUE

Release Notes:

- N/A
2025-03-18 13:47:56 -04:00
Cole Miller
78d6ae9215 Disable flaky file status test again (#26925)
Failure on an unrelated commit:
https://github.com/zed-industries/zed/actions/runs/13903012863/job/38899239052

Release Notes:

- N/A
2025-03-18 13:47:15 -04:00
Cole Miller
62e41e577d Reinstate failing worktree tests (#26733)
Just debugging for now

Release Notes:

- N/A
2025-03-18 13:46:26 -04:00
Cole Miller
39d0b07913 Fold git merge messages into commit editor placeholder text (#26992)
This PR changes the git commit message editors to surface git's
suggested merge message, if any, as placeholder text, as opposed to
"real" buffer text as was previously the case.

Release Notes:

- Changed git commit message editors to use placeholder text for git's
suggested merge messages
2025-03-18 13:41:56 -04:00
gcp-cherry-pick-bot[bot]
15d3f7c3da Add missing commit event reporting (cherry-pick #26990) (#26993)
Cherry-picked Add missing commit event reporting (#26990)

cc @morgankrey 

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-18 11:31:35 -04:00
gcp-cherry-pick-bot[bot]
da5fcf93d5 git: Always zero panel's entry counts when clearing entries (cherry-pick #26924) (#26965)
Cherry-picked git: Always zero panel's entry counts when clearing
entries (#26924)

Keep the panel's state consistent even when we transition to having no
active repository.

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-18 08:30:16 -04:00
Joseph T. Lyons
a2553d8ea2 Send stable release notes email (#26964)
Release Notes:

- N/A
2025-03-17 23:26:05 -04:00
gcp-cherry-pick-bot[bot]
8be1cc9117 Remove disabling effect on the stage and unstage toolbar buttons (cherry-pick #26936) (#26941)
Cherry-picked Remove disabling effect on the stage and unstage toolbar
buttons (#26936)

Closes #26883

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-17 15:16:24 -04:00
Joseph T. Lyons
e9261f5cc3 zed 0.178.3 2025-03-17 14:08:12 -04:00
gcp-cherry-pick-bot[bot]
7d3ea98d50 Add git.hunk_style setting for gutter hollow hunk behavior (cherry-pick #26816) (#26933)
Cherry-picked Add `git.hunk_style` setting for gutter hollow hunk
behavior (#26816)

This is a follow up to #26809, introducing `git.hunk_style` setting to
control whether staged or unstaged hunks are shown as hollow.

Reused `GitHunkStyleSetting` which was left over from #26504.

Release Notes:

- Added `git.hunk_style` setting to control whether staged or unstaged
hunks are hollow.

Co-authored-by: Jakub Charvat <jakcharvat@gmail.com>
2025-03-17 11:00:22 -07:00
Jakub Charvat
1e979eb53a Update rendering of gutter diff hunks to show whether a hunk is staged or not (#26809)
In the gutter, it seems more intuitive to me for the unstaged hunks to
be hollow, indicating an action left to complete, and the staged hunks
to be filled. I therefore flipped the style of expanded hunks to match
the gutter icons. Is that acceptable? And would it be a breaking change?
If it is not acceptable, then 058dc216d5
contains the opposite behaviour, it is not a problem to revert to it.

In the following images, the first hunk is always ~unstaged~ staged and
the second is ~staged~ unstaged.

<img width="138" alt="image"
src="https://github.com/user-attachments/assets/35927069-da90-424a-8988-a4eb984d865f"
/>
<img width="133" alt="image"
src="https://github.com/user-attachments/assets/4edd0e0d-a2b5-453a-8172-47684e065c82"
/>

<br />
<img width="143" alt="image"
src="https://github.com/user-attachments/assets/2f295944-81aa-45f3-a103-c13b92bc2aba"
/>
<img width="133" alt="image"
src="https://github.com/user-attachments/assets/35248218-7104-4059-8742-ae0e54da6c6b"
/>


Release Notes:

- Improved gutter diff hunks to show whether a hunk is staged
2025-03-17 10:19:06 -07:00
gcp-cherry-pick-bot[bot]
ef71b65b31 Fix the feedback modal (cherry-pick #26793) (#26808)
Cherry-picked Fix the feedback modal (#26793)

Closes #26787

Release Notes:

- Fixed a bug that prevented typing in the in-app feedback form

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-14 18:17:39 -04:00
Joseph T. Lyons
a664c5d4d2 zed 0.178.2 2025-03-14 10:43:58 -04:00
gcp-cherry-pick-bot[bot]
6793a275d1 Show git toasts for 10s (cherry-pick #26714) (#26755)
Cherry-picked Show git toasts for 10s (#26714)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-13 23:25:18 -06:00
gcp-cherry-pick-bot[bot]
2497ede1a0 Revert "Disable automatic window tabbing (cherry-pick #26600) (#26652)" (cherry-pick #26749) (#26752)
Cherry-picked Revert "Disable automatic window tabbing (cherry-pick
#26600) (#26652)" (#26749)

This reverts commit 391eb380b5.

For some reason that is very unclear to me, this broke ssh'ing into
macOS remotes.
The remote process aborts with:

```
-------------------------------------
Translated Report (Full Report Below)
-------------------------------------

Process:               zed-remote-server-dev-build [78088]
Path:                  /Users/USER/*/zed-remote-server-dev-build
Identifier:            zed-remote-server-dev-build
Version:               ???
Code Type:             ARM-64 (Native)
Parent Process:        launchd [1]
Responsible:           iTerm2 [62245]
User ID:               501

Date/Time:             2025-03-13 19:30:37.6827 -0600
OS Version:            macOS 15.3.1 (24D70)
Report Version:        12
Anonymous UUID:        3A9631EB-5468-8CA4-7A0F-E36C3FF9D04F

Sleep/Wake UUID:       C935AE4C-E06A-4F6D-BE97-101E4E03482F

Time Awake Since Boot: 910000 seconds
Time Since Wake:       1265 seconds

System Integrity Protection: enabled

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000

Termination Reason:    Namespace OBJC, Code 1 

Application Specific Information:
crashed on child side of fork pre-exec


Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	       0x18653fc6c __abort_with_payload + 8
1   libsystem_kernel.dylib        	       0x186565eb8 abort_with_payload_wrapper_internal + 104
2   libsystem_kernel.dylib        	       0x186565e50 abort_with_reason + 32
3   libobjc.A.dylib               	       0x1861dc040 _objc_fatalv(unsigned long long, unsigned long long, char const*, char*) + 128
4   libobjc.A.dylib               	       0x1861dbfc0 _objc_fatal(char const*, ...) + 44
5   libobjc.A.dylib               	       0x1861c1674 performForkChildInitialize(objc_class*, objc_class*) + 400
6   libobjc.A.dylib               	       0x1861a67f0 initializeNonMetaClass + 592
7   libobjc.A.dylib               	       0x1861c4a3c initializeAndMaybeRelock(objc_class*, objc_object*, locker_mixin<lockdebug::lock_mixin<objc_lock_base_t>>&, bool) + 164
8   libobjc.A.dylib               	       0x1861a5f98 lookUpImpOrForward + 304
9   libobjc.A.dylib               	       0x1861a5b84 _objc_msgSend_uncached + 68
10  zed-remote-server-dev-build   	       0x104f9ec4c _$LT$$LP$$RP$$u20$as$u20$objc..message..MessageArguments$GT$::invoke::hf68c58806f4b5702 + 56
11  zed-remote-server-dev-build   	       0x104f9d4c8 objc::message::platform::send_unverified::h2ec8392957fd6551 + 120
12  zed-remote-server-dev-build   	       0x104e5631c cocoa::appkit::NSPasteboard::generalPasteboard::h68122d7f32549cba + 512
13  zed-remote-server-dev-build   	       0x104e3b3b4 gpui::platform::mac::platform::MacPlatform::new::hb68d7ae2c5fdea7e + 336
14  zed-remote-server-dev-build   	       0x104e48008 gpui::platform::current_platform::h931999673c8c6468 + 28
15  zed-remote-server-dev-build   	       0x104ee4284 gpui::app::Application::headless::h3bffec62c65240ce + 32
16  zed-remote-server-dev-build   	       0x1023746ac remote_server::unix::execute_run::h7ac8de1a7e257f61 + 1200
17  zed-remote-server-dev-build   	       0x102368e1c remote_server::main::h42e4b18462b32dcf + 252 (main.rs:56)
18  zed-remote-server-dev-build   	       0x10236717c core::ops::function::FnOnce::call_once::h8534244cea12c898 + 16 (function.rs:250)
19  zed-remote-server-dev-build   	       0x102368154 std::sys::backtrace::__rust_begin_short_backtrace::h22fd48e0f46eb10b + 12 (backtrace.rs:152)
20  zed-remote-server-dev-build   	       0x10236bf74 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hf8bd0081bf8d785b + 16 (rt.rs:195)
21  zed-remote-server-dev-build   	       0x105723d20 std::rt::lang_start_internal::h5f91760815528aa2 + 1092
22  zed-remote-server-dev-build   	       0x10236bf50 std::rt::lang_start::hb88fe48ac1498ea6 + 60 (rt.rs:194)
23  zed-remote-server-dev-build   	       0x10236b67c main + 36
24  dyld                          	       0x1861f4274 start + 2840
```

Which is not even (apparently) on the line that calls this function.

To reproduce this, run `ZED_BUILD_REMOTE_SERVER=true cargo run
ssh://127.0.0.1/~/`.

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-13 21:36:25 -06:00
gcp-cherry-pick-bot[bot]
1c600880f6 terminal: Fix issues with highlighted ranges of paths (cherry-pick #26695) (#26697)
Cherry-picked terminal: Fix issues with highlighted ranges of paths
(#26695)

Fixes a few problems,

- Uses `Boundary::Grid` instead of `Boundary::Cursor` for highlighted
range adjustments.

This fixes quite a few wierd behaviors around highlighting paths that
had to be scrolled into view (i.e. were in the terminal history)
including the issue described in the release notes as well as a
regression caused by #26401 where the highlight range would span from
the start of the path to the cursor location in the shell prompt

- Strips all trailing `:`s from the paths, updating the highlighted
range accordingly.

This worked fine before and is just a visual improvement.


Release Notes:

- Fixed an issue where file paths in the terminal surrounded by `()` or
`[]` would not be highlighted properly

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-03-13 21:41:08 -05:00
gcp-cherry-pick-bot[bot]
d4062a8537 Allow parsing commits when we can't resolve the permalink (cherry-pick #26709) (#26736)
Cherry-picked Allow parsing commits when we can't resolve the permalink
(#26709)

Closes #26577

Release Notes:

- git: Fix showing commit messages for all repos

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-13 18:33:51 -04:00
Cole Miller
fe46a11dab worktree: Disable flaky test_file_status test (#26729)
See also:
- https://github.com/zed-industries/zed/pull/26684
- https://github.com/zed-industries/zed/pull/26710

Release Notes:

- N/A
2025-03-13 17:32:57 -04:00
gcp-cherry-pick-bot[bot]
8d2843c6ff Don't render "Initialize Repository" button when no worktrees (cherry-pick #26713) (#26728)
Cherry-picked Don't render "Initialize Repository" button when no
worktrees (#26713)

Closes #26676  

Release Notes:

- Fixed the git panel to not show an "Initialize Repositories" button in
empty projects

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-13 17:21:43 -04:00
gcp-cherry-pick-bot[bot]
ee65485197 Fix flicker when reverting last hunk from the project diff view (cherry-pick #26706) (#26712)
Cherry-picked Fix flicker when reverting last hunk from the project diff
view (#26706)

Closes #26696

Closes #ISSUE

Release Notes:

- git: Fix flicker when reverting last hunk in project diff view

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-13 16:55:36 -04:00
gcp-cherry-pick-bot[bot]
d0354400f0 Use system git for committing (cherry-pick #26705) (#26726)
Cherry-picked Use system git for committing (#26705)

Closes #26472

Release Notes:

- On macOS, switched to using the system's git binary to create commits.
This fixes issues that some users were seeing with pre-commit hooks.
Compatibility note: after this change, it is no longer possible to
commit from Zed unless git is installed.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-13 16:49:28 -04:00
gcp-cherry-pick-bot[bot]
d7c5808af1 git: Fix race condition when [un]staging hunks in quick succession (cherry-pick #26422) (#26719)
Cherry-picked git: Fix race condition when [un]staging hunks in quick
succession (#26422)

- [x] Fix `[un]stage` hunk operations cancelling pending ones
  - [x] Add test
- [ ] bugs I stumbled upon (try to repro again before merging)
  - [x] holding `git::StageAndNext` skips hunks randomly 
    - [x] Add test
  - [x] restoring a file keeps it in the git panel
- [x] Double clicking on `toggle staged` fast makes Zed disagree with
`git` CLI
- [x] checkbox shows ✔️ (fully staged) after a single
stage

Release Notes:

- N/A

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Max <max@zed.dev>
2025-03-13 17:18:56 -03:00
Peter Tripp
7dd9bab46f worktree: Disable flaky tests (test_write_file, test_git_status_postprocessing) (#26710)
Comment out flaky tests:
- `worktree_tests::test_write_file`
- `worktree_tests::test_git_status_postprocessing`

Job links:
- windows fail:
https://github.com/zed-industries/zed/actions/runs/13841766606/job/38730766252
- macos fail:
https://github.com/zed-industries/zed/actions/runs/13841766606/job/38730764118

That
[commit](85384fb9c6)
was a non-op script change, but in the [prior
commit](00359271d1)
[windows/macos
pass](https://github.com/zed-industries/zed/actions/runs/13841135221).

Similar experience with `worktree_tests::test_write_file` on both macOS
windows too.

- See also: https://github.com/zed-industries/zed/pull/26684

Release Notes:

- N/A
2025-03-13 15:17:40 -04:00
Peter Tripp
73640a4797 ci: Fix 'Run Tests' not always running (#26685)
Follow up to:
- https://github.com/zed-industries/zed/pull/26551

We need the "Tests Pass" step to run `if: always()`. 
Turns out when it's 'skipped', it counts as 'passing' with respect to
required status checks.


Release Notes:

- N/A
2025-03-13 15:04:08 -04:00
Peter Tripp
52a854ba6e ci: Use smaller windows runners (#26674)
Let's see if the speed of `windows-2025-32` for `windows_tests` is
fast-enough for PRs and everywhere else use `windows-2025-16`. Leaving
`windows_clippy` unchanged with `windows-2025-16`.

Release Notes:

- N/A
2025-03-13 15:04:02 -04:00
gcp-cherry-pick-bot[bot]
c26e354fce Fix being unable to put a cursor after trailing deletion hunks (cherry-pick #26621) (#26698)
Cherry-picked Fix being unable to put a cursor after trailing deletion
hunks (#26621)

Closes #26541

Release Notes:

- Fixed a bug that prevented putting the cursor after a deletion hunk at
the end of a file, in the absence of trailing newlines

---------

Co-authored-by: Max <max@zed.dev>

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Max <max@zed.dev>
2025-03-13 14:16:19 -04:00
Marshall Bowers
5f087750fe worktree: Disable flaky test_git_repository_status test (#26684)
This PR disables the flaky `test_git_repository_status` test.

Release Notes:

- N/A
2025-03-13 12:17:49 -04:00
Joseph T. Lyons
ab04489eb3 zed 0.178.1 2025-03-13 10:53:33 -04:00
gcp-cherry-pick-bot[bot]
20456e0b0f theme: Fix incorrect version control keys in One themes (cherry-pick #26606) (#26624)
Cherry-picked theme: Fix incorrect version control keys in One themes
(#26606)

While the `.{variants}` of the theme keys _were_ incorrect, they are
actually more consistent with our current theme keys (thanks AI!) So we
will keep theme, and fix the incorrect usages in the one themes and
elsewhere.

Old description:
> 
> This PR fixes an issue where we specified the incorrect theme keys
(thanks AI!) > in the theme schema. The following keys have been changed
to their correct > versions:
> 
> | Before                        | After                   |
> |-------------------------------|-------------------------|
> | version_control.added         | version_control_added   |
> | version_control.deleted       | version_control_deleted |
> | version_control.modified      | version_control_modified|
> | version_control.renamed       | version_control_renamed |
> | version_control.conflict      | version_control_conflict|
> | version_control.ignored       | version_control_ignored |
> 
> Please use the after versions in your themes, as they are correct! 
> 
> We won't be adding secondary keys to fix this automatically as git
only > officially launched today.
> 
> Due to this change, we've also updated the version control keys in the
One > themes to keep the default diff hunks looks from changing.

Closes #26572

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly.

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2025-03-13 09:55:33 -04:00
Nate Butler
01f39e99cc gruvbox: version_control_ -> version_control. (#26665)
Missed this in PR #26606 

Before:

![CleanShot 2025-03-13 at 08 58
59@2x](https://github.com/user-attachments/assets/021df4b1-5a70-4fae-a109-9b8bb35949e3)

After:

![CleanShot 2025-03-13 at 08 59
22@2x](https://github.com/user-attachments/assets/01dca26d-77ec-4a54-8b7c-aa2fb160ff7d)

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly. (again)
2025-03-13 09:53:54 -04:00
gcp-cherry-pick-bot[bot]
391eb380b5 Disable automatic window tabbing (cherry-pick #26600) (#26652)
Cherry-picked macOS: Disable automatic window tabbing in fullscreen mode
(#26600)

Fixes #26534 (this time for real)

Release Notes:

- Fixed issue where Zed would behave weirdly when opening new fullscreen
windows by disabling window tabbing

Apple docs:

https://developer.apple.com/documentation/appkit/nswindow/allowsautomaticwindowtabbing

Co-authored-by: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com>
2025-03-13 11:20:43 +02:00
gcp-cherry-pick-bot[bot]
cdfa3dd922 Properly handle goto single file worktrees during terminal cmd-clicks (cherry-pick #26582) (#26651)
Cherry-picked Properly handle goto single file worktrees during terminal
cmd-clicks (#26582)

Closes https://github.com/zed-industries/zed/issues/26431
Follow-up of https://github.com/zed-industries/zed/pull/26174

`path_with_position.path.strip_prefix(&worktree_root)` used in the PR is
wrong for cases of single-file worktrees, where it will return empty
paths that will result in incorrect project and FS entries accessed.

Release Notes:

- Fixed goto single file worktrees during terminal cmd-clicks

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-13 11:20:36 +02:00
gcp-cherry-pick-bot[bot]
7364f81172 Improve terminal hover tooltips (cherry-pick #26487) (#26650)
Cherry-picked Improve terminal hover tooltips (#26487)

Follow-up of https://github.com/zed-industries/zed/pull/26174

* Fixes `./path/foo.bar` not properly parsed as valid open target
* Shows full open target's path in cmd-hover tooltips

Before:

<img width="864" alt="before_1"

src="https://github.com/user-attachments/assets/2575b887-6c4d-486e-8e92-dd76aedf8103"
/>
<img width="864" alt="before_2"

src="https://github.com/user-attachments/assets/ded1f203-523c-4b75-afe9-fe541c785798"
/>

After:

<img width="864" alt="after_1"

src="https://github.com/user-attachments/assets/c50d9ba3-5dfb-4cfb-aed6-00e6fa6f088e"
/>
<img width="864" alt="after_2"

src="https://github.com/user-attachments/assets/0cdc8f34-7faa-4aab-87f3-dc0c8b499842"
/>

Release Notes:



- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-13 10:59:37 +02:00
Mikayla Maki
44447e288c Rename the editor::ToggleGitBlame action to git::Blame (#26565)
Release Notes:

- Git Beta: Renamed `editor::ToggleGitBlame` to `git::Blame`

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-12 22:35:25 -04:00
Conrad Irwin
fc76d08057 Fix overflow in create branch label (#26591)
Closes #ISSUE

Release Notes:

- N/A
2025-03-12 22:33:28 -04:00
Peter Tripp
be0595a5ca ci: Fix tests not-running on main (#26613)
Follow-up to #26551 

Fix for tests being skipped on main.
Also fetch less history: [example
run](https://github.com/zed-industries/zed/actions/runs/13822318758/job/38670334893)

Release Notes:

- N/A
2025-03-12 19:53:45 -04:00
Mikayla Maki
1b6421b3c9 Fix a bug where the modal layer could not be dismissed by the mouse 2025-03-12 16:46:46 -07:00
Peter Tripp
cb675c773f ci: GitHub actions refactor (#26551)
Refactor GitHub actions CI workflow.
- Single combined 'tests_pass' action so we only need one mandatory
check for merge queue
- Add new `job_spec` job which determines what needs to be run (+5secs)
  - Do not run full CI for docs only changes (~30secs vs 10+mins)
- Only run `script/generate-licenses` if Cargo.lock changed (saves
~23secs on mac_test)
- Move prettier /docs check to ci.yml and remove docs.yml 
- Run Windows tests on every PR commit
- Added new Windows runners named to reflect their OS/capacity
(windows-2025-64, windows-2025-32, windows-2025-16)

Release Notes:

- N/A
2025-03-12 17:57:55 -04:00
gcp-cherry-pick-bot[bot]
209f1da94a git: Hard wrap in editor (cherry-pick #26507) (#26589)
Cherry-picked git: Hard wrap in editor  (#26507)

This adds the ability for the editor to implement hard wrap (similar to
"textwidth" in vim).

If you are typing and your line extends beyond the limit, a newline is
inserted before the most recent space on the line. If you are otherwise
editing the line, pasting, etc. then you will need to manually rewrap.

Release Notes:

- git: Commit messages are now wrapped "as you type" to 72 characters.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-12 17:45:11 -04:00
gcp-cherry-pick-bot[bot]
96e1eeda41 Fix message on push (cherry-pick #26588) (#26595)
Cherry-picked Fix message on push (#26588)

Instead of saying "Successfully pushed new branch" we say "Pushed x to
y"

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-12 17:41:38 -04:00
gcp-cherry-pick-bot[bot]
4994b020e2 Git on main thread (cherry-pick #26573) (#26586)
Cherry-picked Git on main thread (#26573)

This moves spawning of the git subprocess to the main thread. We're not
yet
sure why, but when we spawn a process using GCD's background queues,
sub-processes like git-credential-manager fail to open windows.

This seems to be fixable either by using the main thread, or by using a
standard background thread,
but for now we use the main thread.


Release Notes:

- Git: Fix git-credential-manager

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-03-12 14:00:47 -06:00
gcp-cherry-pick-bot[bot]
5bc7479fb8 Fix unstage/stage in project diff not working when git panel isn't open (cherry-pick #26575) (#26581)
Cherry-picked Fix unstage/stage in project diff not working when git
panel isn't open (#26575)

Closes #ISSUE

Release Notes:

- Fix Bug where unstage/stage all in project diff wouldn't work while
git panel was closed

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-12 13:51:57 -06:00
Joseph T. Lyons
7b821e9e97 v0.178.x preview 2025-03-12 12:52:45 -04:00
46 changed files with 2075 additions and 1272 deletions

View File

@@ -23,9 +23,47 @@ env:
RUST_BACKTRACE: 1
jobs:
job_spec:
name: Decide which jobs to run
if: github.repository_owner == 'zed-industries'
outputs:
run_tests: ${{ steps.filter.outputs.run_tests }}
runs-on:
- ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
# 350 is arbitrary; ~10days of history on main (5secs); full history is ~25secs
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
- name: Fetch git history and generate output filters
id: filter
run: |
if [ -z "$GITHUB_BASE_REF" ]; then
echo "Not in a PR context (i.e., push to main/stable/preview)"
COMPARE_REV=$(git rev-parse HEAD~1)
else
echo "In a PR context comparing to pull_request.base.ref"
git fetch origin "$GITHUB_BASE_REF" --depth=350
COMPARE_REV=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -v "^docs/") ]]; then
echo "run_tests=true" >> $GITHUB_OUTPUT
else
echo "run_tests=false" >> $GITHUB_OUTPUT
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
echo "run_license=true" >> $GITHUB_OUTPUT
else
echo "run_license=false" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
timeout-minutes: 60
runs-on:
- self-hosted
@@ -69,6 +107,7 @@ jobs:
style:
timeout-minutes: 60
name: Check formatting and spelling
needs: [job_spec]
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-8vcpu-ubuntu-2204
@@ -76,6 +115,21 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Prettier Check on /docs
working-directory: ./docs
run: |
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
echo "To fix, run from the root of the zed repo:"
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
env:
PRETTIER_VERSION: 3.5.0
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos
@@ -91,7 +145,10 @@ jobs:
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- self-hosted
- test
@@ -123,7 +180,9 @@ jobs:
- name: Check licenses
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
if [[ "${{ needs.job_spec.outputs.run_license }}" == "true" ]]; then
script/generate-licenses /tmp/zed_licenses_output
fi
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
@@ -154,7 +213,10 @@ jobs:
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
@@ -203,9 +265,12 @@ jobs:
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
if: github.repository_owner == 'zed-industries'
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- buildjet-16vcpu-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -239,21 +304,12 @@ jobs:
windows_clippy:
timeout-minutes: 60
name: (Windows) Run Clippy
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-2
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: windows-2025-16
steps:
# Temporarily Collect some metadata about the hardware behind our runners.
- name: GHA Runner Info
run: |
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
ConvertTo-Json -Depth 10 |
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
@{
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
} | ConvertTo-Json
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
@@ -306,21 +362,13 @@ jobs:
windows_tests:
timeout-minutes: 60
name: (Windows) Run Tests
if: ${{ github.repository_owner == 'zed-industries' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'windows')) }}
runs-on: hosted-windows-2
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
# Use bigger runners for PRs (speed); smaller for async (cost)
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
steps:
# Temporarily Collect some metadata about the hardware behind our runners.
- name: GHA Runner Info
run: |
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
ConvertTo-Json -Depth 10 |
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
@{
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
} | ConvertTo-Json
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
@@ -372,13 +420,50 @@ jobs:
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
}
tests_pass:
name: Tests Pass
runs-on: ubuntu-latest
needs:
- job_spec
- style
- migration_checks
- linux_tests
- build_remote_server
- macos_tests
- windows_clippy
- windows_tests
if: always()
steps:
- name: Check all tests passed
run: |
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration checks failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
fi
exit $RET_CODE
bundle-mac:
timeout-minutes: 120
name: Create a macOS bundle
runs-on:
- self-hosted
- bundle
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [macos_tests]
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@@ -468,7 +553,9 @@ jobs:
name: Linux x86_x64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -485,7 +572,7 @@ jobs:
run: ./script/linux && ./script/install-mold 2.34.0
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
if: startsWith(github.ref, 'refs/tags/v')
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
@@ -495,14 +582,18 @@ jobs:
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload Linux remote server to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-x86_64.gz
@@ -523,7 +614,9 @@ jobs:
name: Linux arm64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -540,7 +633,7 @@ jobs:
run: ./script/linux
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
if: startsWith(github.ref, 'refs/tags/v')
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
@@ -550,14 +643,18 @@ jobs:
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload Linux remote server to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
if: |
github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-aarch64.gz
@@ -575,7 +672,9 @@ jobs:
auto-release-preview:
name: Auto release preview
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
if: |
startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
runs-on:
- self-hosted

View File

@@ -13,11 +13,12 @@ jobs:
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview/latest"
URL="https://zed.dev/releases/preview/latest"
else
URL="https://zed.dev/releases/stable/latest"
URL="https://zed.dev/releases/stable/latest"
fi
echo "::set-output name=URL::$URL"
echo "URL=$URL" >> $GITHUB_OUTPUT
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
@@ -33,3 +34,34 @@ jobs:
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ steps.get-content.outputs.string }}
send_release_notes_email:
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
echo "was_promoted_from_preview=true" >> $GITHUB_OUTPUT
else
echo "was_promoted_from_preview=false" >> $GITHUB_OUTPUT
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"version": "${{ github.event.release.tag_name }}",
"markdown_body": ${{ toJSON(github.event.release.body) }}
}'

View File

@@ -1,39 +0,0 @@
name: Docs
on:
pull_request:
paths:
- "docs/**"
push:
branches:
- main
jobs:
check_formatting:
name: "Check formatting"
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Prettier Check on /docs
working-directory: ./docs
run: |
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
echo "To fix, run from the root of the zed repo:"
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
env:
PRETTIER_VERSION: 3.5.0
- name: Check for Typos with Typos-CLI
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
with:
config: ./typos.toml
files: ./docs/

2
Cargo.lock generated
View File

@@ -17008,7 +17008,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.178.0"
version = "0.178.4"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -850,7 +850,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 filled and staged hunks hollow:
// "hunk_style": "staged_hollow"
// 2. Show unstaged hunks hollow and staged hunks filled:
// "hunk_style": "unstaged_hollow"
"hunk_style": "staged_hollow"
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.

View File

@@ -6,15 +6,7 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -105,9 +97,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -399,15 +391,7 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -498,9 +482,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -792,15 +776,7 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -891,9 +867,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -1185,15 +1161,7 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1284,9 +1252,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1578,15 +1546,7 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1677,9 +1637,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1971,15 +1931,7 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -2070,9 +2022,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",

View File

@@ -96,9 +96,9 @@
"terminal.ansi.bright_white": "#dce0e5ff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control_added": "#a7c088ff",
"version_control_modified": "#dec184ff",
"version_control_deleted": "#d07277ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",
@@ -475,9 +475,9 @@
"terminal.ansi.bright_white": "#242529ff",
"terminal.ansi.dim_white": "#97979aff",
"link_text.hover": "#5c78e2ff",
"version_control_added": "#669f59ff",
"version_control_modified": "#a48819ff",
"version_control_deleted": "#d36151ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",
"conflict.border": "#f4e7d1ff",

View File

@@ -22,6 +22,7 @@ git2.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true
sum_tree.workspace = true
text.workspace = true
@@ -31,7 +32,6 @@ util.workspace = true
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
serde_json.workspace = true
text = { workspace = true, features = ["test-support"] }

View File

@@ -6,9 +6,9 @@ use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use sum_tree::{SumTree, TreeMap};
use text::ToOffset as _;
use sum_tree::SumTree;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use text::{AnchorRangeExt, ToOffset as _};
use util::ResultExt;
pub struct BufferDiff {
@@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
pending_hunks: TreeMap<usize, PendingHunk>,
pending_hunks: SumTree<PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
}
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
pub enum DiffHunkSecondaryStatus {
HasSecondaryHunk,
OverlapsWithSecondaryHunk,
None,
NoSecondaryHunk,
SecondaryHunkAdditionPending,
SecondaryHunkRemovalPending,
}
@@ -74,6 +74,8 @@ struct InternalDiffHunk {
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus,
}
@@ -93,6 +95,16 @@ impl sum_tree::Item for InternalDiffHunk {
}
}
impl sum_tree::Item for PendingHunk {
type Summary = DiffHunkSummary;
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
}
}
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
@@ -176,6 +188,7 @@ impl BufferDiffSnapshot {
}
impl BufferDiffInner {
/// Returns the new index text and new pending hunks.
fn stage_or_unstage_hunks(
&mut self,
unstaged_diff: &Self,
@@ -183,7 +196,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
) -> (Option<Rope>, SumTree<PendingHunk>) {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
@@ -195,41 +208,41 @@ impl BufferDiffInner {
// entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(_, head_text @ _) => {
if stage {
(index_text, head_text) => {
let (rope, new_status) = if stage {
log::debug!("stage all");
return (
(
file_exists.then(|| buffer.as_rope().clone()),
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
},
)],
);
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
)
} else {
log::debug!("unstage all");
return (
(
head_text,
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
},
)],
);
}
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
)
};
let hunk = PendingHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
buffer_version: buffer.version().clone(),
new_status,
};
let tree = SumTree::from_item(hunk, buffer);
return (rope, tree);
}
};
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut edits = Vec::new();
let mut pending_hunks = Vec::new();
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = unstaged_diff
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk {
buffer_range,
diff_base_byte_range,
@@ -237,12 +250,58 @@ impl BufferDiffInner {
..
} in hunks.iter().cloned()
{
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
let preceding_pending_hunks =
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
pending_hunks.append(preceding_pending_hunks, buffer);
// skip all overlapping old pending hunks
while old_pending_hunks
.item()
.is_some_and(|preceding_pending_hunk_item| {
preceding_pending_hunk_item
.buffer_range
.overlaps(&buffer_range, buffer)
})
{
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
continue;
}
pending_hunks.push(
PendingHunk {
buffer_range,
diff_base_byte_range,
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
buffer,
);
}
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
for PendingHunk {
buffer_range,
diff_base_byte_range,
..
} in pending_hunks.iter().cloned()
{
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
@@ -294,22 +353,15 @@ impl BufferDiffInner {
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
};
pending_hunks.push((
diff_base_byte_range.start,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
));
edits.push((index_range, replacement_text));
}
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
@@ -354,12 +406,14 @@ impl BufferDiffInner {
});
let mut secondary_cursor = None;
let mut pending_hunks = TreeMap::default();
let mut pending_hunks_cursor = None;
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
secondary_cursor = Some(cursor);
pending_hunks = secondary.pending_hunks.clone();
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
}
let max_point = buffer.max_point();
@@ -378,16 +432,33 @@ impl BufferDiffInner {
end_anchor = buffer.anchor_before(end_point);
}
let mut secondary_status = DiffHunkSecondaryStatus::None;
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false;
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if start_anchor
.cmp(&pending_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
}
if let Some(pending_hunk) = pending_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
}
}
@@ -449,7 +520,7 @@ impl BufferDiffInner {
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.
secondary_status: DiffHunkSecondaryStatus::None,
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
})
})
}
@@ -724,7 +795,7 @@ impl BufferDiff {
base_text,
hunks,
base_text_exists,
pending_hunks: TreeMap::default(),
pending_hunks: SumTree::new(&buffer),
}
}
}
@@ -740,8 +811,8 @@ impl BufferDiff {
cx.background_spawn(async move {
BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer),
pending_hunks: TreeMap::default(),
base_text_exists,
}
})
@@ -751,7 +822,7 @@ impl BufferDiff {
BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer),
pending_hunks: TreeMap::default(),
pending_hunks: SumTree::new(buffer),
base_text_exists: false,
}
}
@@ -767,7 +838,7 @@ impl BufferDiff {
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks.clear();
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
});
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
@@ -783,18 +854,17 @@ impl BufferDiff {
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
buffer,
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
for (offset, pending_hunk) in pending_hunks {
diff.inner.pending_hunks.insert(offset, pending_hunk);
}
diff.inner.pending_hunks = new_pending_hunks;
});
}
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
@@ -916,7 +986,9 @@ impl BufferDiff {
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
};
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
self.inner = new_state;
if !base_text_changed {
self.inner.pending_hunks = pending_hunks;
@@ -1149,21 +1221,21 @@ impl DiffHunkStatus {
pub fn deleted_none() -> Self {
Self {
kind: DiffHunkStatusKind::Deleted,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
pub fn added_none() -> Self {
Self {
kind: DiffHunkStatusKind::Added,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
pub fn modified_none() -> Self {
Self {
kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
}
@@ -1171,13 +1243,14 @@ impl DiffHunkStatus {
/// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn assert_hunks<Iter>(
diff_hunks: Iter,
pub fn assert_hunks<ExpectedText, HunkIter>(
diff_hunks: HunkIter,
buffer: &text::BufferSnapshot,
diff_base: &str,
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
) where
Iter: Iterator<Item = DiffHunk>,
HunkIter: Iterator<Item = DiffHunk>,
ExpectedText: AsRef<str>,
{
let actual_hunks = diff_hunks
.map(|hunk| {
@@ -1197,14 +1270,14 @@ pub fn assert_hunks<Iter>(
.map(|(r, old_text, new_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
*old_text,
new_text.to_string(),
old_text.as_ref(),
new_text.as_ref().to_string(),
*status,
)
})
.collect();
assert_eq!(actual_hunks, expected_hunks);
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
}
#[cfg(test)]
@@ -1263,7 +1336,7 @@ mod tests {
);
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
assert_hunks(
assert_hunks::<&str, _>(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
&buffer,
&diff_base,
@@ -1601,7 +1674,10 @@ mod tests {
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
assert_ne!(
hunk.secondary_status,
DiffHunkSecondaryStatus::NoSecondaryHunk
)
}
let new_index_text = diff
@@ -1880,10 +1956,10 @@ mod tests {
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::None;
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
true
}
DiffHunkSecondaryStatus::None => {
DiffHunkSecondaryStatus::NoSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
false
}

View File

@@ -2038,7 +2038,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
// client_b now requests git blame for the open buffer
editor_b.update_in(cx_b, |editor_b, window, cx| {
assert!(editor_b.blame().is_none());
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
editor_b.toggle_git_blame(&git::Blame {}, window, cx);
});
cx_a.executor().run_until_parked();

View File

@@ -6770,7 +6770,7 @@ async fn test_remote_git_branches(
assert_eq!(branches_b, branches_set);
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
.await
.unwrap()
.unwrap();
@@ -6790,15 +6790,23 @@ async fn test_remote_git_branches(
assert_eq!(host_branch.name, branches[2]);
// Also try creating a new branch
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.create_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.change_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
executor.run_until_parked();

View File

@@ -294,7 +294,7 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(&branches_b, &branches_set);
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
.await
.unwrap()
.unwrap();
@@ -316,15 +316,23 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(server_branch.name, branches[2]);
// Also try creating a new branch
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.create_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.change_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
executor.run_until_parked();

View File

@@ -412,7 +412,6 @@ gpui::actions!(
Tab,
Backtab,
ToggleAutoSignatureHelp,
ToggleGitBlame,
ToggleGitBlameInline,
ToggleIndentGuides,
ToggleInlayHints,

View File

@@ -607,12 +607,6 @@ pub trait Addon: 'static {
fn to_any(&self) -> &dyn std::any::Any;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum IsVimMode {
Yes,
No,
}
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
///
/// See the [module level documentation](self) for more information.
@@ -644,6 +638,7 @@ pub struct Editor {
inline_diagnostics_enabled: bool,
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
hard_wrap: Option<usize>,
// TODO: make this a access method
pub project: Option<Entity<Project>>,
@@ -1355,6 +1350,7 @@ impl Editor {
inline_diagnostics_update: Task::ready(()),
inline_diagnostics: Vec::new(),
soft_wrap_mode_override,
hard_wrap: None,
completion_provider: project.clone().map(|project| Box::new(project) as _),
semantics_provider: project.clone().map(|project| Rc::new(project) as _),
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
@@ -3192,6 +3188,19 @@ impl Editor {
let trigger_in_words =
this.show_edit_predictions_in_menu() || !had_active_inline_completion;
if this.hard_wrap.is_some() {
let latest: Range<Point> = this.selections.newest(cx).range();
if latest.is_empty()
&& this
.buffer()
.read(cx)
.snapshot(cx)
.line_len(MultiBufferRow(latest.start.row))
== latest.start.column
{
this.rewrap_impl(true, cx)
}
}
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
this.refresh_inline_completion(true, false, window, cx);
@@ -8507,10 +8516,10 @@ impl Editor {
}
pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context<Self>) {
self.rewrap_impl(IsVimMode::No, cx)
self.rewrap_impl(false, cx)
}
pub fn rewrap_impl(&mut self, is_vim_mode: IsVimMode, cx: &mut Context<Self>) {
pub fn rewrap_impl(&mut self, override_language_settings: bool, cx: &mut Context<Self>) {
let buffer = self.buffer.read(cx).snapshot(cx);
let selections = self.selections.all::<Point>(cx);
let mut selections = selections.iter().peekable();
@@ -8584,7 +8593,9 @@ impl Editor {
RewrapBehavior::Anywhere => true,
};
let should_rewrap = is_vim_mode == IsVimMode::Yes || allow_rewrap_based_on_language;
let should_rewrap = override_language_settings
|| allow_rewrap_based_on_language
|| self.hard_wrap.is_some();
if !should_rewrap {
continue;
}
@@ -8632,9 +8643,11 @@ impl Editor {
continue;
};
let wrap_column = buffer
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize;
let wrap_column = self.hard_wrap.unwrap_or_else(|| {
buffer
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize
});
let wrapped_text = wrap_with_prefix(
line_prefix,
lines_without_prefixes.join(" "),
@@ -8645,7 +8658,7 @@ impl Editor {
// TODO: should always use char-based diff while still supporting cursor behavior that
// matches vim.
let mut diff_options = DiffOptions::default();
if is_vim_mode == IsVimMode::Yes {
if override_language_settings {
diff_options.max_word_diff_len = 0;
diff_options.max_word_diff_line_count = 0;
} else {
@@ -14215,6 +14228,11 @@ impl Editor {
cx.notify();
}
pub fn set_hard_wrap(&mut self, hard_wrap: Option<usize>, cx: &mut Context<Self>) {
self.hard_wrap = hard_wrap;
cx.notify();
}
pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
self.text_style_refinement = Some(style);
}
@@ -14497,7 +14515,7 @@ impl Editor {
pub fn toggle_git_blame(
&mut self,
_: &ToggleGitBlame,
_: &::git::Blame,
window: &mut Window,
cx: &mut Context<Self>,
) {

View File

@@ -4737,6 +4737,31 @@ async fn test_rewrap(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_hard_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_editor(|editor, _, cx| {
editor.set_hard_wrap(Some(14), cx);
});
cx.set_state(indoc!(
"
one two three ˇ
"
));
cx.simulate_input("four");
cx.run_until_parked();
cx.assert_editor_state(indoc!(
"
one two three
fourˇ
"
));
}
#[gpui::test]
async fn test_clipboard(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -55,7 +55,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::{
@@ -4415,7 +4415,7 @@ impl EditorElement {
}),
};
if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
// Flatten the background color with the editor color to prevent
// elements below transparent hunks from showing through
let flattened_background_color = cx
@@ -4424,13 +4424,29 @@ impl EditorElement {
.editor_background
.blend(background_color);
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_background_color,
Edges::default(),
transparent_black(),
));
if !Self::diff_hunk_hollow(status, cx) {
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_background_color,
Edges::default(),
transparent_black(),
));
} else {
let flattened_unstaged_background_color = cx
.theme()
.colors()
.editor_background
.blend(background_color.opacity(0.3));
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_unstaged_background_color,
Edges::all(Pixels(1.0)),
flattened_background_color,
));
}
}
}
});
@@ -5635,6 +5651,18 @@ impl EditorElement {
&[run],
)
}
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
let unstaged = status.has_secondary_hunk();
let unstaged_hollow = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| {
matches!(style, GitHunkStyleSetting::UnstagedHollow)
});
unstaged == unstaged_hollow
}
}
fn header_jump_data(
@@ -6786,10 +6814,9 @@ impl Element for EditorElement {
}
};
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let staged_highlight = LineHighlight {
let hollow_highlight = LineHighlight {
background: (background_color.opacity(if is_light {
0.08
} else {
@@ -6803,13 +6830,13 @@ impl Element for EditorElement {
}),
};
let unstaged_highlight =
let filled_highlight =
solid_background(background_color.opacity(hunk_opacity)).into();
let background = if unstaged {
unstaged_highlight
let background = if Self::diff_hunk_hollow(diff_status, cx) {
hollow_highlight
} else {
staged_highlight
filled_highlight
};
highlighted_rows

View File

@@ -488,7 +488,7 @@ async fn parse_commit_messages(
},
))
} else {
continue;
None
};
let remote = parsed_remote_url

View File

@@ -2,8 +2,8 @@ use crate::commit::get_messages;
use crate::Oid;
use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet};
use futures::AsyncWriteExt;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::Stdio;
use std::{ops::Range, path::Path};
use text::Rope;
@@ -21,14 +21,14 @@ pub struct Blame {
}
impl Blame {
pub fn for_path(
pub async fn for_path(
git_binary: &Path,
working_directory: &Path,
path: &Path,
content: &Rope,
remote_url: Option<String>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, content)?;
let output = run_git_blame(git_binary, working_directory, path, content).await?;
let mut entries = parse_git_blame(&output)?;
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
@@ -39,8 +39,9 @@ impl Blame {
}
let shas = unique_shas.into_iter().collect::<Vec<_>>();
let messages =
get_messages(working_directory, &shas).context("failed to get commit messages")?;
let messages = get_messages(working_directory, &shas)
.await
.context("failed to get commit messages")?;
Ok(Self {
entries,
@@ -53,13 +54,13 @@ impl Blame {
const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
fn run_git_blame(
async fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
path: &Path,
contents: &Rope,
) -> Result<String> {
let child = util::command::new_std_command(git_binary)
let mut child = util::command::new_smol_command(git_binary)
.current_dir(working_directory)
.arg("blame")
.arg("--incremental")
@@ -72,18 +73,19 @@ fn run_git_blame(
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let mut stdin = child
let stdin = child
.stdin
.as_ref()
.as_mut()
.context("failed to get pipe to stdin of git blame command")?;
for chunk in contents.chunks() {
stdin.write_all(chunk.as_bytes())?;
stdin.write_all(chunk.as_bytes()).await?;
}
stdin.flush()?;
stdin.flush().await?;
let output = child
.wait_with_output()
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {

View File

@@ -3,20 +3,21 @@ use anyhow::{anyhow, Result};
use collections::HashMap;
use std::path::Path;
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
if shas.is_empty() {
return Ok(HashMap::default());
}
const MARKER: &str = "<MARKER>";
let output = util::command::new_std_command("git")
let output = util::command::new_smol_command("git")
.current_dir(working_directory)
.arg("show")
.arg("-s")
.arg(format!("--format=%B{}", MARKER))
.args(shas.iter().map(ToString::to_string))
.output()
.await
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
anyhow::ensure!(

View File

@@ -54,8 +54,10 @@ actions!(
Init,
]
);
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);
/// The length of a Git short SHA.
pub const SHORT_SHA_LENGTH: usize = 7;

File diff suppressed because it is too large Load Diff

View File

@@ -205,9 +205,9 @@ impl BranchListDelegate {
return;
};
cx.spawn(|_, cx| async move {
cx.update(|cx| repo.read(cx).create_branch(&new_branch_name))?
cx.update(|cx| repo.read(cx).create_branch(new_branch_name.to_string()))?
.await??;
cx.update(|cx| repo.read(cx).change_branch(&new_branch_name))?
cx.update(|cx| repo.read(cx).change_branch(new_branch_name.to_string()))?
.await??;
Ok(())
})
@@ -358,7 +358,7 @@ impl PickerDelegate for BranchListDelegate {
let cx = cx.to_async();
anyhow::Ok(async move {
cx.update(|cx| repo.read(cx).change_branch(&branch.name))?
cx.update(|cx| repo.read(cx).change_branch(branch.name.to_string()))?
.await?
})
})??;
@@ -434,6 +434,7 @@ impl PickerDelegate for BranchListDelegate {
"Create branch \"{}\"",
entry.branch.name
))
.single_line()
.into_any_element()
} else {
HighlightedLabel::new(

View File

@@ -146,7 +146,7 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_commit_message = panel.suggest_commit_message();
let suggested_commit_message = panel.suggest_commit_message(cx);
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);

View File

@@ -367,6 +367,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
let placeholder = placeholder.unwrap_or("Enter commit message");
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor
@@ -1412,7 +1413,7 @@ impl GitPanel {
return Some(message.to_string());
}
self.suggest_commit_message()
self.suggest_commit_message(cx)
.filter(|message| !message.trim().is_empty())
}
@@ -1501,15 +1502,17 @@ impl GitPanel {
telemetry::event!("Git Uncommitted");
let confirmation = self.check_for_pushed_commits(window, cx);
let prior_head = self.load_commit_details("HEAD", cx);
let prior_head = self.load_commit_details("HEAD".to_string(), cx);
let task = cx.spawn_in(window, |this, mut cx| async move {
let result = maybe!(async {
if let Ok(true) = confirmation.await {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, cx| repo.reset("HEAD^", ResetMode::Soft, cx))?
.await??;
repo.update(&mut cx, |repo, cx| {
repo.reset("HEAD^".to_string(), ResetMode::Soft, cx)
})?
.await??;
Ok(Some(prior_head))
} else {
@@ -1579,7 +1582,15 @@ impl GitPanel {
}
/// Suggests a commit message based on the changed files and their statuses
pub fn suggest_commit_message(&self) -> Option<String> {
pub fn suggest_commit_message(&self, cx: &App) -> Option<String> {
if let Some(merge_message) = self
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).merge_message.as_ref())
{
return Some(merge_message.clone());
}
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
Some(staged_entry)
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
@@ -1715,19 +1726,6 @@ impl GitPanel {
}));
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_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(placeholder_text), cx)
});
cx.notify();
}
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.can_push_and_pull(cx) {
return;
@@ -2184,7 +2182,6 @@ impl GitPanel {
git_panel.clear_pending();
}
git_panel.update_visible_entries(cx);
git_panel.update_editor_placeholder(cx);
git_panel.update_scrollbar_properties(window, cx);
})
.ok();
@@ -2220,7 +2217,7 @@ impl GitPanel {
git_panel.commit_editor = cx.new(|cx| {
commit_message_editor(
buffer,
git_panel.suggest_commit_message().as_deref(),
git_panel.suggest_commit_message(cx).as_deref(),
git_panel.project.clone(),
true,
window,
@@ -2240,7 +2237,15 @@ impl GitPanel {
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
self.entries.clear();
self.single_staged_entry.take();
self.single_staged_entry.take();
self.single_tracked_entry.take();
self.conflicted_count = 0;
self.conflicted_staged_count = 0;
self.new_count = 0;
self.tracked_count = 0;
self.new_staged_count = 0;
self.tracked_staged_count = 0;
self.entry_count = 0;
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
@@ -2392,6 +2397,15 @@ impl GitPanel {
self.select_first_entry_if_none(cx);
let suggested_commit_message = self.suggest_commit_message(cx);
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(placeholder_text), cx)
});
cx.notify();
}
@@ -2915,6 +2929,10 @@ impl GitPanel {
.disabled(!can_commit || self.modal_open)
.on_click({
cx.listener(move |this, _: &ClickEvent, window, cx| {
telemetry::event!(
"Git Committed",
source = "Git Panel"
);
this.commit_changes(window, cx)
})
}),
@@ -3045,21 +3063,24 @@ impl GitPanel {
"No Git repositories"
},
))
.children(self.active_repository.is_none().then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
}))
.children({
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
})
})
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
@@ -3401,7 +3422,7 @@ impl GitPanel {
fn load_commit_details(
&self,
sha: &str,
sha: String,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<CommitDetails>> {
let Some(repo) = self.active_repository.clone() else {
@@ -3911,7 +3932,7 @@ impl GitPanelMessageTooltip {
cx.spawn_in(window, |this, mut cx| async move {
let details = git_panel
.update(&mut cx, |git_panel, cx| {
git_panel.load_commit_details(&sha, cx)
git_panel.load_commit_details(sha.to_string(), cx)
})?
.await?;

View File

@@ -278,7 +278,7 @@ impl ProjectDiff {
has_staged_hunks = true;
has_unstaged_hunks = true;
}
DiffHunkSecondaryStatus::None
DiffHunkSecondaryStatus::NoSecondaryHunk
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
has_staged_hunks = true;
}
@@ -308,7 +308,7 @@ impl ProjectDiff {
fn handle_editor_event(
&mut self,
_: &Entity<Editor>,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -330,6 +330,11 @@ impl ProjectDiff {
}
_ => {}
}
if editor.focus_handle(cx).contains_focused(window, cx) {
if self.multibuffer.read(cx).is_empty() {
self.focus_handle.focus(window)
}
}
}
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
@@ -815,23 +820,30 @@ impl ProjectDiffToolbar {
cx.dispatch_action(action.as_ref());
})
}
fn dispatch_panel_action(
&self,
action: &dyn Action,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn stage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.workspace
.read_with(cx, |workspace, cx| {
.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<GitPanel>(cx) {
panel.focus_handle(cx).focus(window)
panel.update(cx, |panel, cx| {
panel.stage_all(&Default::default(), window, cx);
});
}
})
.ok();
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}
fn unstage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.workspace
.update(cx, |workspace, cx| {
let Some(panel) = workspace.panel::<GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.unstage_all(&Default::default(), window, cx);
});
})
.ok();
}
}
@@ -912,12 +924,6 @@ impl Render for ProjectDiffToolbar {
&StageAndNext,
&focus_handle,
))
// don't actually disable the button so it's mashable
.color(if button_states.stage {
Color::Default
} else {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&StageAndNext, window, cx)
})),
@@ -929,11 +935,6 @@ impl Render for ProjectDiffToolbar {
&UnstageAndNext,
&focus_handle,
))
.color(if button_states.unstage {
Color::Default
} else {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&UnstageAndNext, window, cx)
})),
@@ -985,7 +986,7 @@ impl Render for ProjectDiffToolbar {
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_panel_action(&UnstageAll, window, cx)
this.unstage_all(window, cx)
})),
)
},
@@ -1005,7 +1006,7 @@ impl Render for ProjectDiffToolbar {
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_panel_action(&StageAll, window, cx)
this.stage_all(window, cx)
})),
),
)

View File

@@ -143,7 +143,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
}
} else {
SuccessMessage {
message: "Successfully pushed new branch".to_owned(),
message: format!("Pushed {} to {}", branch_name, remote_ref.name),
style: SuccessStyle::ToastWithLog { output },
}
}

View File

@@ -2007,22 +2007,17 @@ impl MultiBuffer {
cx: &App,
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
let snapshot = self.read(cx);
let point = point.to_point(&snapshot);
let mut cursor = snapshot.cursor::<Point>();
cursor.seek(&point);
cursor.region().and_then(|region| {
if !region.is_main_buffer {
return None;
}
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let buffer = self.buffers.borrow()[&region.buffer.remote_id()]
let (buffer, point, is_main_buffer) =
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
Some((
self.buffers
.borrow()
.get(&buffer.remote_id())?
.buffer
.clone();
Some((buffer, buffer_point, region.excerpt.id))
})
.clone(),
point,
is_main_buffer,
))
}
pub fn buffer_point_to_anchor(
@@ -4176,22 +4171,36 @@ impl MultiBufferSnapshot {
let region = cursor.region()?;
let overshoot = offset - region.range.start;
let buffer_offset = region.buffer_range.start + overshoot;
if buffer_offset > region.buffer.len() {
if buffer_offset == region.buffer.len() + 1
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
} else if buffer_offset > region.buffer.len() {
return None;
}
Some((region.buffer, buffer_offset))
}
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
pub fn point_to_buffer_point(
&self,
point: Point,
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
let mut cursor = self.cursor::<Point>();
cursor.seek(&point);
let region = cursor.region()?;
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
if buffer_point > region.buffer.max_point() {
let excerpt = cursor.excerpt()?;
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
} else if buffer_point > region.buffer.max_point() {
return None;
}
Some((region.buffer, buffer_point, region.is_main_buffer))
Some((region.buffer, buffer_point, excerpt.id))
}
pub fn suggested_indents(
@@ -4733,6 +4742,9 @@ impl MultiBufferSnapshot {
.buffer
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
);
if point == region.range.end.key && region.has_trailing_newline {
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
}
return Some(position);
} else {
return Some(D::from_text_summary(&self.text_summary()));

View File

@@ -3121,6 +3121,100 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
assert_eq!(point_2, Point::new(3, 0));
}
#[gpui::test]
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
let base_text_1 = "one\ntwo".to_owned();
let text_1 = "one\n".to_owned();
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
cx.run_until_parked();
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
multibuffer.add_diff(diff_1.clone(), cx);
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
multibuffer
});
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
(multibuffer.snapshot(cx), multibuffer.subscribe())
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
"
),
);
assert_eq!(snapshot.max_point(), Point::new(2, 0));
assert_eq!(snapshot.len(), 8);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(translated_offset, "one\n".len());
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(translated_point, Point::new(1, 0));
// The same, for an excerpt that's not at the end of the multibuffer.
let text_2 = "foo\n".to_owned();
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 0),
primary: None,
}],
cx,
);
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
foo
"
),
);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_offset, "one\n".len());
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_point, Point::new(1, 0));
}
fn format_diff(
text: &str,
row_infos: &Vec<RowInfo>,
@@ -3379,16 +3473,12 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
}
}
let point = snapshot.max_point();
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
return;
};
assert!(offset <= buffer.len(),);
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
return;
};
assert!(point <= buffer.max_point(),);
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
assert!(offset <= buffer.len());
}
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
assert!(point <= buffer.max_point());
}
}
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {

View File

@@ -837,52 +837,63 @@ impl LocalBufferStore {
let snapshot =
worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?;
let diff_bases_changes_by_buffer = cx
.background_spawn(async move {
diff_state_updates
.into_iter()
.filter_map(|(buffer, path, current_index_text, current_head_text)| {
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let index_text = if current_index_text.is_some() {
local_repo.repo().load_index_text(&relative_path)
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo.repo().load_committed_text(&relative_path)
} else {
None
};
.spawn(async move |cx| {
let mut results = Vec::new();
for (buffer, path, current_index_text, current_head_text) in diff_state_updates
{
let Some(local_repo) = snapshot.local_repo_for_path(&path) else {
continue;
};
let Some(relative_path) = local_repo.relativize(&path).ok() else {
continue;
};
let index_text = if current_index_text.is_some() {
local_repo
.repo()
.load_index_text(relative_path.clone(), cx.clone())
.await
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo
.repo()
.load_committed_text(relative_path, cx.clone())
.await
} else {
None
};
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
return None;
}
continue;
}
}
let diff_bases_change =
match (current_index_text.is_some(), current_head_text.is_some()) {
(true, true) => Some(if index_text == head_text {
DiffBasesChange::SetBoth(head_text)
} else {
DiffBasesChange::SetEach {
index: index_text,
head: head_text,
}
}),
(true, false) => Some(DiffBasesChange::SetIndex(index_text)),
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
(false, false) => None,
};
let diff_bases_change =
match (current_index_text.is_some(), current_head_text.is_some()) {
(true, true) => Some(if index_text == head_text {
DiffBasesChange::SetBoth(head_text)
} else {
DiffBasesChange::SetEach {
index: index_text,
head: head_text,
}
}),
(true, false) => Some(DiffBasesChange::SetIndex(index_text)),
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
(false, false) => None,
};
Some((buffer, diff_bases_change))
})
.collect::<Vec<_>>()
results.push((buffer, diff_bases_change))
}
results
})
.await;
@@ -1620,11 +1631,12 @@ impl BufferStore {
anyhow::Ok(Some((repo, relative_path, content)))
});
cx.background_spawn(async move {
cx.spawn(|cx| async move {
let Some((repo, relative_path, content)) = blame_params? else {
return Ok(None);
};
repo.blame(&relative_path, content)
repo.blame(relative_path.clone(), content, cx)
.await
.with_context(|| format!("Failed to blame {:?}", relative_path.0))
.map(Some)
})

View File

@@ -318,20 +318,9 @@ impl GitStore {
}
// Update the statuses and merge message but keep everything else.
let existing_handle = handle.clone();
existing_handle.update(cx, |existing_handle, cx| {
existing_handle.update(cx, |existing_handle, _| {
existing_handle.repository_entry = repo.clone();
if matches!(git_repo, GitRepo::Local { .. })
&& existing_handle.merge_message != merge_message
{
if let (Some(merge_message), Some(buffer)) =
(&merge_message, &existing_handle.commit_message_buffer)
{
buffer.update(cx, |buffer, cx| {
if buffer.is_empty() {
buffer.set_text(merge_message.as_str(), cx);
}
})
}
if matches!(git_repo, GitRepo::Local { .. }) {
existing_handle.merge_message = merge_message;
}
});
@@ -401,7 +390,7 @@ impl GitStore {
if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo.update(cx, |repo, cx| {
repo.set_index_text(
&path,
path,
new_index_text.as_ref().map(|rope| rope.to_string()),
cx,
)
@@ -715,7 +704,7 @@ impl GitStore {
repository_handle
.update(&mut cx, |repository_handle, cx| {
repository_handle.set_index_text(
&RepoPath::from_str(&envelope.payload.path),
RepoPath::from_str(&envelope.payload.path),
envelope.payload.text,
cx,
)
@@ -808,7 +797,7 @@ impl GitStore {
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.create_branch(&branch_name)
repository_handle.create_branch(branch_name)
})?
.await??;
@@ -828,7 +817,7 @@ impl GitStore {
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.change_branch(&branch_name)
repository_handle.change_branch(branch_name)
})?
.await??;
@@ -847,7 +836,7 @@ impl GitStore {
let commit = repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.show(&envelope.payload.commit)
repository_handle.show(envelope.payload.commit)
})?
.await??;
Ok(proto::GitCommitDetails {
@@ -876,7 +865,7 @@ impl GitStore {
repository_handle
.update(&mut cx, |repository_handle, cx| {
repository_handle.reset(&envelope.payload.commit, mode, cx)
repository_handle.reset(envelope.payload.commit, mode, cx)
})?
.await??;
Ok(proto::Ack {})
@@ -1081,8 +1070,8 @@ impl Repository {
fn send_job<F, Fut, R>(&self, job: F) -> oneshot::Receiver<R>
where
F: FnOnce(GitRepo) -> Fut + 'static,
Fut: Future<Output = R> + Send + 'static,
F: FnOnce(GitRepo, AsyncApp) -> Fut + 'static,
Fut: Future<Output = R> + 'static,
R: Send + 'static,
{
self.send_keyed_job(None, job)
@@ -1090,8 +1079,8 @@ impl Repository {
fn send_keyed_job<F, Fut, R>(&self, key: Option<GitJobKey>, job: F) -> oneshot::Receiver<R>
where
F: FnOnce(GitRepo) -> Fut + 'static,
Fut: Future<Output = R> + Send + 'static,
F: FnOnce(GitRepo, AsyncApp) -> Fut + 'static,
Fut: Future<Output = R> + 'static,
R: Send + 'static,
{
let (result_tx, result_rx) = futures::channel::oneshot::channel();
@@ -1100,8 +1089,8 @@ impl Repository {
.unbounded_send(GitJob {
key,
job: Box::new(|cx: &mut AsyncApp| {
let job = job(git_repo);
cx.background_spawn(async move {
let job = job(git_repo, cx.clone());
cx.spawn(|_| async move {
let result = job.await;
result_tx.send(result).ok();
})
@@ -1257,7 +1246,6 @@ impl Repository {
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
let merge_message = self.merge_message.clone();
cx.spawn(|repository, mut cx| async move {
let buffer = buffer_store
.update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
@@ -1270,12 +1258,6 @@ impl Repository {
})?;
}
if let Some(merge_message) = merge_message {
buffer.update(&mut cx, |buffer, cx| {
buffer.set_text(merge_message.as_str(), cx)
})?;
}
repository.update(&mut cx, |repository, _| {
repository.commit_message_buffer = Some(buffer.clone());
})?;
@@ -1292,9 +1274,9 @@ impl Repository {
let commit = commit.to_string();
let env = self.worktree_environment(cx);
self.send_job(|git_repo| async move {
self.send_job(|git_repo, _| async move {
match git_repo {
GitRepo::Local(repo) => repo.checkout_files(&commit, &paths, &env.await),
GitRepo::Local(repo) => repo.checkout_files(commit, paths, env.await).await,
GitRepo::Remote {
project_id,
client,
@@ -1322,17 +1304,17 @@ impl Repository {
pub fn reset(
&self,
commit: &str,
commit: String,
reset_mode: ResetMode,
cx: &mut App,
) -> oneshot::Receiver<Result<()>> {
let commit = commit.to_string();
let env = self.worktree_environment(cx);
self.send_job(|git_repo| async move {
self.send_job(|git_repo, _| async move {
match git_repo {
GitRepo::Local(git_repo) => {
let env = env.await;
git_repo.reset(&commit, reset_mode, &env)
git_repo.reset(commit, reset_mode, env).await
}
GitRepo::Remote {
project_id,
@@ -1359,11 +1341,10 @@ impl Repository {
})
}
pub fn show(&self, commit: &str) -> oneshot::Receiver<Result<CommitDetails>> {
let commit = commit.to_string();
self.send_job(|git_repo| async move {
pub fn show(&self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
self.send_job(|git_repo, cx| async move {
match git_repo {
GitRepo::Local(git_repository) => git_repository.show(&commit),
GitRepo::Local(git_repository) => git_repository.show(commit, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1433,9 +1414,9 @@ impl Repository {
let env = env.await;
this.update(&mut cx, |this, _| {
this.send_job(|git_repo| async move {
this.send_job(|git_repo, cx| async move {
match git_repo {
GitRepo::Local(repo) => repo.stage_paths(&entries, &env),
GitRepo::Local(repo) => repo.stage_paths(entries, env, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1504,9 +1485,9 @@ impl Repository {
let env = env.await;
this.update(&mut cx, |this, _| {
this.send_job(|git_repo| async move {
this.send_job(|git_repo, cx| async move {
match git_repo {
GitRepo::Local(repo) => repo.unstage_paths(&entries, &env),
GitRepo::Local(repo) => repo.unstage_paths(entries, env, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1587,17 +1568,11 @@ impl Repository {
cx: &mut App,
) -> oneshot::Receiver<Result<()>> {
let env = self.worktree_environment(cx);
self.send_job(|git_repo| async move {
self.send_job(|git_repo, cx| async move {
match git_repo {
GitRepo::Local(repo) => {
let env = env.await;
repo.commit(
message.as_ref(),
name_and_email
.as_ref()
.map(|(name, email)| (name.as_ref(), email.as_ref())),
&env,
)
repo.commit(message, name_and_email, env, cx).await
}
GitRepo::Remote {
project_id,
@@ -1634,12 +1609,12 @@ impl Repository {
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
let env = self.worktree_environment(cx);
self.send_job(move |git_repo| async move {
self.send_job(move |git_repo, cx| async move {
match git_repo {
GitRepo::Local(git_repository) => {
let askpass = AskPassSession::new(&executor, askpass).await?;
let env = env.await;
git_repository.fetch(askpass, &env)
git_repository.fetch(askpass, env, cx).await
}
GitRepo::Remote {
project_id,
@@ -1685,12 +1660,21 @@ impl Repository {
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
let env = self.worktree_environment(cx);
self.send_job(move |git_repo| async move {
self.send_job(move |git_repo, cx| async move {
match git_repo {
GitRepo::Local(git_repository) => {
let env = env.await;
let askpass = AskPassSession::new(&executor, askpass).await?;
git_repository.push(&branch, &remote, options, askpass, &env)
git_repository
.push(
branch.to_string(),
remote.to_string(),
options,
askpass,
env,
cx,
)
.await
}
GitRepo::Remote {
project_id,
@@ -1740,12 +1724,14 @@ impl Repository {
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
let env = self.worktree_environment(cx);
self.send_job(move |git_repo| async move {
self.send_job(move |git_repo, cx| async move {
match git_repo {
GitRepo::Local(git_repository) => {
let askpass = AskPassSession::new(&executor, askpass).await?;
let env = env.await;
git_repository.pull(&branch, &remote, askpass, &env)
git_repository
.pull(branch.to_string(), remote.to_string(), askpass, env, cx)
.await
}
GitRepo::Remote {
project_id,
@@ -1781,18 +1767,17 @@ impl Repository {
fn set_index_text(
&self,
path: &RepoPath,
path: RepoPath,
content: Option<String>,
cx: &mut App,
) -> oneshot::Receiver<anyhow::Result<()>> {
let path = path.clone();
let env = self.worktree_environment(cx);
self.send_keyed_job(
Some(GitJobKey::WriteIndex(path.clone())),
|git_repo| async move {
|git_repo, cx| async move {
match git_repo {
GitRepo::Local(repo) => repo.set_index_text(&path, content, &env.await),
GitRepo::Local(repo) => repo.set_index_text(path, content, env.await, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1819,11 +1804,9 @@ impl Repository {
&self,
branch_name: Option<String>,
) -> oneshot::Receiver<Result<Vec<Remote>>> {
self.send_job(|repo| async move {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => {
git_repository.get_remotes(branch_name.as_deref())
}
GitRepo::Local(git_repository) => git_repository.get_remotes(branch_name, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1854,9 +1837,13 @@ impl Repository {
}
pub fn branches(&self) -> oneshot::Receiver<Result<Vec<Branch>>> {
self.send_job(|repo| async move {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.branches(),
GitRepo::Local(git_repository) => {
let git_repository = git_repository.clone();
cx.background_spawn(async move { git_repository.branches().await })
.await
}
GitRepo::Remote {
project_id,
client,
@@ -1884,9 +1871,9 @@ impl Repository {
}
pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
self.send_job(|repo| async move {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.diff(diff_type),
GitRepo::Local(git_repository) => git_repository.diff(diff_type, cx).await,
GitRepo::Remote {
project_id,
client,
@@ -1916,11 +1903,12 @@ impl Repository {
})
}
pub fn create_branch(&self, branch_name: &str) -> oneshot::Receiver<Result<()>> {
let branch_name = branch_name.to_owned();
self.send_job(|repo| async move {
pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.create_branch(&branch_name),
GitRepo::Local(git_repository) => {
git_repository.create_branch(branch_name, cx).await
}
GitRepo::Remote {
project_id,
client,
@@ -1942,11 +1930,12 @@ impl Repository {
})
}
pub fn change_branch(&self, branch_name: &str) -> oneshot::Receiver<Result<()>> {
let branch_name = branch_name.to_owned();
self.send_job(|repo| async move {
pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.change_branch(&branch_name),
GitRepo::Local(git_repository) => {
git_repository.change_branch(branch_name, cx).await
}
GitRepo::Remote {
project_id,
client,
@@ -1969,9 +1958,9 @@ impl Repository {
}
pub fn check_for_pushed_commits(&self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
self.send_job(|repo| async move {
self.send_job(|repo, cx| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.check_for_pushed_commit(),
GitRepo::Local(git_repository) => git_repository.check_for_pushed_commit(cx).await,
GitRepo::Remote {
project_id,
client,

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: staged_hollow
pub hunk_style: Option<GitHunkStyleSetting>,
}
impl GitSettings {
@@ -203,20 +207,11 @@ 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
/// Show unstaged hunks with a filled background and staged hunks hollow.
#[default]
Transparent,
/// Show unstaged hunks with a pattern background
Pattern,
/// Show unstaged hunks with a border background
Border,
/// Show staged hunks with a pattern background
StagedPattern,
/// Show staged hunks with a pattern background
StagedTransparent,
/// Show staged hunks with a pattern background
StagedBorder,
StagedHollow,
/// Show unstaged hunks hollow and staged hunks with a filled background.
UnstagedHollow,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension.
let _ = project
project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
})
@@ -6008,7 +6008,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
0..0,
"// the-deleted-contents\n",
"",
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::None),
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
)],
);
});
@@ -6168,7 +6168,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
@@ -6217,7 +6222,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
@@ -6256,7 +6266,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
@@ -6277,6 +6292,223 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
} else {
panic!("Unexpected event {event:?}");
}
// Allow writing to the git index to succeed again.
fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
// Stage two hunks with separate operations.
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
});
// Both staged hunks appear as pending.
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
),
],
);
});
// Both staging operations take effect.
cx.run_until_parked();
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
],
);
});
}
#[allow(clippy::format_collect)]
#[gpui::test]
async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
use DiffHunkSecondaryStatus::*;
init_test(cx);
let different_lines = (0..500)
.step_by(5)
.map(|i| format!("diff {}\n", i))
.collect::<Vec<String>>();
let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::<String>();
let file_contents = (0..500)
.map(|i| {
if i % 5 == 0 {
different_lines[i / 5].clone()
} else {
format!("{}\n", i)
}
})
.collect::<String>();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
json!({
".git": {},
"file.txt": file_contents.clone()
}),
)
.await;
fs.set_head_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
fs.set_index_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/file.txt", cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let uncommitted_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.unwrap();
let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point());
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
.step_by(5)
.map(|i| {
(
i as u32..i as u32 + 1,
format!("{}\n", i),
different_lines[i / 5].clone(),
DiffHunkStatus::modified(HasSecondaryHunk),
)
})
.collect();
// The hunks are initially unstaged
uncommitted_diff.read_with(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(SecondaryHunkRemovalPending);
}
// Stage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
.hunks_intersecting_range(range.clone(), &snapshot, cx)
.collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
}
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
// If we wait, we'll have no pending hunks
cx.run_until_parked();
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(NoSecondaryHunk);
}
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(SecondaryHunkAdditionPending);
}
// Unstage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
.hunks_intersecting_range(range, &snapshot, cx)
.collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
}
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
// If we wait, we'll have no pending hunks, again
cx.run_until_parked();
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(HasSecondaryHunk);
}
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
}
#[gpui::test]

View File

@@ -1361,7 +1361,7 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
assert_eq!(&remote_branches, &branches_set);
cx.update(|cx| repository.read(cx).change_branch(new_branch))
cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
.await
.unwrap()
.unwrap();
@@ -1383,15 +1383,23 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
assert_eq!(server_branch.name, branches[2]);
// Also try creating a new branch
cx.update(|cx| repository.read(cx).create_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx.update(|cx| {
repository
.read(cx)
.create_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx.update(|cx| repository.read(cx).change_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx.update(|cx| {
repository
.read(cx)
.change_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx.run_until_parked();

View File

@@ -406,6 +406,7 @@ where
self.seek_internal(pos, bias, &mut (), cx)
}
/// Advances the cursor and returns traversed items as a tree.
#[track_caller]
pub fn slice<Target>(
&mut self,

View File

@@ -225,6 +225,15 @@ impl<T: Item> SumTree<T> {
}))
}
/// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context.
pub fn from_summary(summary: T::Summary) -> Self {
SumTree(Arc::new(Node::Leaf {
summary,
items: ArrayVec::new(),
item_summaries: ArrayVec::new(),
}))
}
pub fn from_item(item: T, cx: &<T::Summary as Summary>::Context) -> Self {
let mut tree = Self::new(cx);
tree.push(item, cx);

View File

@@ -935,12 +935,19 @@ impl Terminal {
if is_path_surrounded_by_common_symbols(&file_path) {
word_match = Match::new(
word_match.start().add(term, Boundary::Cursor, 1),
word_match.end().sub(term, Boundary::Cursor, 1),
word_match.start().add(term, Boundary::Grid, 1),
word_match.end().sub(term, Boundary::Grid, 1),
);
file_path = file_path[1..file_path.len() - 1].to_owned();
}
while file_path.ends_with(':') {
file_path.pop();
word_match = Match::new(
*word_match.start(),
word_match.end().sub(term, Boundary::Grid, 1),
);
}
let mut colon_count = 0;
for c in file_path.chars() {
if c == ':' {
@@ -966,7 +973,7 @@ impl Terminal {
let stripped_len = file_path.len() - last_index;
word_match = Match::new(
*word_match.start(),
word_match.end().sub(term, Boundary::Cursor, stripped_len),
word_match.end().sub(term, Boundary::Grid, stripped_len),
);
file_path = file_path[0..last_index].to_owned();
}

View File

@@ -1064,18 +1064,36 @@ fn possible_open_target(
for worktree in &worktree_candidates {
let worktree_root = worktree.read(cx).abs_path();
let paths_to_check = potential_paths
.iter()
.map(|path_with_position| PathWithPosition {
path: path_with_position
.path
.strip_prefix(&worktree_root)
.unwrap_or(&path_with_position.path)
.to_owned(),
row: path_with_position.row,
column: path_with_position.column,
})
.collect::<Vec<_>>();
let mut paths_to_check = Vec::with_capacity(potential_paths.len());
for path_with_position in &potential_paths {
if worktree_root.ends_with(&path_with_position.path) {
let root_path_with_posiition = PathWithPosition {
path: worktree_root.to_path_buf(),
row: path_with_position.row,
column: path_with_position.column,
};
match worktree.read(cx).root_entry() {
Some(root_entry) => {
return Task::ready(Some(OpenTarget::Worktree(
root_path_with_posiition,
root_entry.clone(),
)))
}
None => paths_to_check.push(root_path_with_posiition),
}
} else {
paths_to_check.push(PathWithPosition {
path: path_with_position
.path
.strip_prefix(&worktree_root)
.unwrap_or(&path_with_position.path)
.to_owned(),
row: path_with_position.row,
column: path_with_position.column,
});
};
}
let mut traversal = worktree
.read(cx)

View File

@@ -136,6 +136,7 @@ where
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
}
impl AnchorRangeExt for Range<Anchor> {
@@ -145,4 +146,8 @@ impl AnchorRangeExt for Range<Anchor> {
ord => ord,
}
}
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
}
}

View File

@@ -1,6 +1,6 @@
use crate::{motion::Motion, object::Object, state::Mode, Vim};
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Bias, Editor, IsVimMode};
use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Bias, Editor};
use gpui::{actions, Context, Window};
use language::SelectionGoal;
@@ -14,7 +14,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
vim.update_editor(window, cx, |vim, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut positions = vim.save_selection_starts(editor, cx);
editor.rewrap_impl(IsVimMode::Yes, cx);
editor.rewrap_impl(true, cx);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
@@ -52,7 +52,7 @@ impl Vim {
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
});
editor.rewrap_impl(IsVimMode::Yes, cx);
editor.rewrap_impl(true, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
@@ -83,7 +83,7 @@ impl Vim {
object.expand_selection(map, selection, around);
});
});
editor.rewrap_impl(IsVimMode::Yes, cx);
editor.rewrap_impl(true, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();

View File

@@ -1,4 +1,7 @@
use gpui::{AnyView, DismissEvent, Entity, FocusHandle, Focusable as _, ManagedView, Subscription};
use gpui::{
AnyView, DismissEvent, Entity, FocusHandle, Focusable as _, ManagedView, MouseButton,
Subscription,
};
use ui::prelude::*;
#[derive(Debug)]
@@ -172,11 +175,13 @@ impl Render for ModalLayer {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
el.bg(background)
.occlude()
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.hide_modal(window, cx);
}))
})
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
this.hide_modal(window, cx);
}),
)
.child(
v_flex()
.h(px(0.0))
@@ -185,7 +190,14 @@ impl Render for ModalLayer {
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
.child(
h_flex()
.occlude()
.child(active_modal.modal.view())
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
}),
),
)
}
}

View File

@@ -8,7 +8,7 @@ use ui::{animation::DefaultAnimations, prelude::*};
use crate::Workspace;
const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400);
const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10);
const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
actions!(toast, [RunAction]);

View File

@@ -61,7 +61,7 @@ use std::{
path::{Component, Path, PathBuf},
pin::Pin,
sync::{
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
Arc,
},
time::{Duration, Instant},
@@ -1055,13 +1055,13 @@ impl Worktree {
Worktree::Local(this) => {
let path = Arc::from(path);
let snapshot = this.snapshot();
cx.background_spawn(async move {
cx.spawn(|cx| async move {
if let Some(repo) = snapshot.repository_for_path(&path) {
if let Some(repo_path) = repo.relativize(&path).log_err() {
if let Some(git_repo) =
snapshot.git_repositories.get(&repo.work_directory_id)
{
return Ok(git_repo.repo_ptr.load_index_text(&repo_path));
return Ok(git_repo.repo_ptr.load_index_text(repo_path, cx).await);
}
}
}
@@ -1079,13 +1079,16 @@ impl Worktree {
Worktree::Local(this) => {
let path = Arc::from(path);
let snapshot = this.snapshot();
cx.background_spawn(async move {
cx.spawn(|cx| async move {
if let Some(repo) = snapshot.repository_for_path(&path) {
if let Some(repo_path) = repo.relativize(&path).log_err() {
if let Some(git_repo) =
snapshot.git_repositories.get(&repo.work_directory_id)
{
return Ok(git_repo.repo_ptr.load_committed_text(&repo_path));
return Ok(git_repo
.repo_ptr
.load_committed_text(repo_path, cx)
.await);
}
}
}
@@ -1522,6 +1525,7 @@ impl LocalWorktree {
fs,
fs_case_sensitive,
status_updates_tx: scan_states_tx,
scans_running: Arc::new(AtomicI32::new(0)),
executor: background,
scan_requests_rx,
path_prefixes_to_scan_rx,
@@ -4246,11 +4250,6 @@ struct PathEntry {
scan_id: usize,
}
#[derive(Debug, Default)]
struct FsScanned {
status_scans: Arc<AtomicU32>,
}
impl sum_tree::Item for PathEntry {
type Summary = PathEntrySummary;
@@ -4318,6 +4317,7 @@ struct BackgroundScanner {
fs: Arc<dyn Fs>,
fs_case_sensitive: bool,
status_updates_tx: UnboundedSender<ScanState>,
scans_running: Arc<AtomicI32>,
executor: BackgroundExecutor,
scan_requests_rx: channel::Receiver<ScanRequest>,
path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
@@ -4425,13 +4425,13 @@ impl BackgroundScanner {
// Perform an initial scan of the directory.
drop(scan_job_tx);
let scans_running = self.scan_dirs(true, scan_job_rx).await;
self.scan_dirs(true, scan_job_rx).await;
{
let mut state = self.state.lock();
state.snapshot.completed_scan_id = state.snapshot.scan_id;
}
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
self.send_status_update(scanning, SmallVec::new());
// Process any any FS events that occurred while performing the initial scan.
@@ -4458,7 +4458,7 @@ impl BackgroundScanner {
// these before handling changes reported by the filesystem.
request = self.next_scan_request().fuse() => {
let Ok(request) = request else { break };
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
if !self.process_scan_request(request, scanning).await {
return;
}
@@ -4481,7 +4481,7 @@ impl BackgroundScanner {
self.process_events(vec![abs_path]).await;
}
}
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
self.send_status_update(scanning, request.done);
}
@@ -4675,7 +4675,7 @@ impl BackgroundScanner {
.await;
self.update_ignore_statuses(scan_job_tx).await;
let scans_running = self.scan_dirs(false, scan_job_rx).await;
self.scan_dirs(false, scan_job_rx).await;
let status_update = if !dot_git_abs_paths.is_empty() {
Some(self.update_git_repositories(dot_git_abs_paths))
@@ -4686,6 +4686,7 @@ impl BackgroundScanner {
let phase = self.phase;
let status_update_tx = self.status_updates_tx.clone();
let state = self.state.clone();
let scans_running = self.scans_running.clone();
self.executor
.spawn(async move {
if let Some(status_update) = status_update {
@@ -4701,7 +4702,7 @@ impl BackgroundScanner {
#[cfg(test)]
state.snapshot.check_git_invariants();
}
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
send_status_update_inner(phase, state, status_update_tx, scanning, SmallVec::new());
})
.detach();
@@ -4726,9 +4727,8 @@ impl BackgroundScanner {
}
drop(scan_job_tx);
}
let scans_running = Arc::new(AtomicU32::new(0));
while let Ok(job) = scan_job_rx.recv().await {
self.scan_dir(&scans_running, &job).await.log_err();
self.scan_dir(&job).await.log_err();
}
!mem::take(&mut self.state.lock().paths_to_scan).is_empty()
@@ -4738,16 +4738,16 @@ impl BackgroundScanner {
&self,
enable_progress_updates: bool,
scan_jobs_rx: channel::Receiver<ScanJob>,
) -> FsScanned {
) {
if self
.status_updates_tx
.unbounded_send(ScanState::Started)
.is_err()
{
return FsScanned::default();
return;
}
let scans_running = Arc::new(AtomicU32::new(1));
inc_scans_running(&self.scans_running);
let progress_update_count = AtomicUsize::new(0);
self.executor
.scoped(|scope| {
@@ -4792,7 +4792,7 @@ impl BackgroundScanner {
// Recursively load directories from the file system.
job = scan_jobs_rx.recv().fuse() => {
let Ok(job) = job else { break };
if let Err(err) = self.scan_dir(&scans_running, &job).await {
if let Err(err) = self.scan_dir(&job).await {
if job.path.as_ref() != Path::new("") {
log::error!("error scanning directory {:?}: {}", job.abs_path, err);
}
@@ -4805,10 +4805,7 @@ impl BackgroundScanner {
})
.await;
scans_running.fetch_sub(1, atomic::Ordering::Release);
FsScanned {
status_scans: scans_running,
}
dec_scans_running(&self.scans_running, 1);
}
fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
@@ -4821,7 +4818,7 @@ impl BackgroundScanner {
)
}
async fn scan_dir(&self, scans_running: &Arc<AtomicU32>, job: &ScanJob) -> Result<()> {
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
let root_abs_path;
let root_char_bag;
{
@@ -4876,7 +4873,7 @@ impl BackgroundScanner {
self.watcher.as_ref(),
);
if let Some(local_repo) = repo {
scans_running.fetch_add(1, atomic::Ordering::Release);
inc_scans_running(&self.scans_running);
git_status_update_jobs
.push(self.schedule_git_statuses_update(&mut state, local_repo));
}
@@ -4999,7 +4996,7 @@ impl BackgroundScanner {
let task_state = self.state.clone();
let phase = self.phase;
let status_updates_tx = self.status_updates_tx.clone();
let scans_running = scans_running.clone();
let scans_running = self.scans_running.clone();
self.executor
.spawn(async move {
if !git_status_update_jobs.is_empty() {
@@ -5007,7 +5004,7 @@ impl BackgroundScanner {
let status_updated = status_updates
.iter()
.any(|update_result| update_result.is_ok());
scans_running.fetch_sub(status_updates.len() as u32, atomic::Ordering::Release);
dec_scans_running(&scans_running, status_updates.len() as i32);
if status_updated {
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
send_status_update_inner(
@@ -5467,6 +5464,7 @@ impl BackgroundScanner {
}
};
inc_scans_running(&self.scans_running);
status_updates
.push(self.schedule_git_statuses_update(&mut state, local_repository));
}
@@ -5499,9 +5497,12 @@ impl BackgroundScanner {
});
}
let scans_running = self.scans_running.clone();
self.executor.spawn(async move {
let _updates_finished: Vec<Result<(), oneshot::Canceled>> =
let updates_finished: Vec<Result<(), oneshot::Canceled>> =
join_all(status_updates).await;
let n = updates_finished.len();
dec_scans_running(&scans_running, n as i32);
})
}
@@ -5509,104 +5510,15 @@ impl BackgroundScanner {
fn schedule_git_statuses_update(
&self,
state: &mut BackgroundScannerState,
mut local_repository: LocalRepositoryEntry,
local_repository: LocalRepositoryEntry,
) -> oneshot::Receiver<()> {
let repository_name = local_repository.work_directory.display_name();
let path_key = local_repository.work_directory.path_key();
let job_state = self.state.clone();
let (tx, rx) = oneshot::channel();
state.repository_scans.insert(
path_key.clone(),
self.executor.spawn(async move {
update_branches(&job_state, &mut local_repository).log_err();
log::trace!("updating git statuses for repo {repository_name}",);
let t0 = Instant::now();
let Some(statuses) = local_repository
.repo()
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
.log_err()
else {
return;
};
log::trace!(
"computed git statuses for repo {repository_name} in {:?}",
t0.elapsed()
);
let t0 = Instant::now();
let mut changed_paths = Vec::new();
let snapshot = job_state.lock().snapshot.snapshot.clone();
let Some(mut repository) = snapshot
.repository(path_key)
.context(
"Tried to update git statuses for a repository that isn't in the snapshot",
)
.log_err()
else {
return;
};
let merge_head_shas = local_repository.repo().merge_head_shas();
if merge_head_shas != local_repository.current_merge_head_shas {
mem::take(&mut repository.current_merge_conflicts);
}
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {
repo_path: repo_path.clone(),
status: *status,
},
&(),
);
if status.is_conflicted() {
repository.current_merge_conflicts.insert(repo_path.clone());
}
if let Some(path) = project_path {
changed_paths.push(path);
}
}
repository.statuses_by_path = new_entries_by_path;
let mut state = job_state.lock();
state
.snapshot
.repositories
.insert_or_replace(repository, &());
state.snapshot.git_repositories.update(
&local_repository.work_directory_id,
|entry| {
entry.current_merge_head_shas = merge_head_shas;
entry.merge_message = std::fs::read_to_string(
local_repository.dot_git_dir_abs_path.join("MERGE_MSG"),
)
.ok()
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
entry.status_scan_id += 1;
},
);
util::extend_sorted(
&mut state.changed_paths,
changed_paths,
usize::MAX,
Ord::cmp,
);
log::trace!(
"applied git status updates for repo {repository_name} in {:?}",
t0.elapsed(),
);
tx.send(()).ok();
}),
local_repository.work_directory.path_key(),
self.executor
.spawn(do_git_status_update(job_state, local_repository, tx)),
);
rx
}
@@ -5638,6 +5550,15 @@ impl BackgroundScanner {
}
}
fn inc_scans_running(scans_running: &AtomicI32) {
scans_running.fetch_add(1, atomic::Ordering::Release);
}
fn dec_scans_running(scans_running: &AtomicI32, by: i32) {
let old = scans_running.fetch_sub(by, atomic::Ordering::Release);
debug_assert!(old >= by);
}
fn send_status_update_inner(
phase: BackgroundScannerPhase,
state: Arc<Mutex<BackgroundScannerState>>,
@@ -5665,11 +5586,11 @@ fn send_status_update_inner(
.is_ok()
}
fn update_branches(
async fn update_branches(
state: &Mutex<BackgroundScannerState>,
repository: &mut LocalRepositoryEntry,
) -> Result<()> {
let branches = repository.repo().branches()?;
let branches = repository.repo().branches().await?;
let snapshot = state.lock().snapshot.snapshot.clone();
let mut repository = snapshot
.repository(repository.work_directory.path_key())
@@ -5685,6 +5606,100 @@ fn update_branches(
Ok(())
}
async fn do_git_status_update(
job_state: Arc<Mutex<BackgroundScannerState>>,
mut local_repository: LocalRepositoryEntry,
tx: oneshot::Sender<()>,
) {
let repository_name = local_repository.work_directory.display_name();
log::trace!("updating git branches for repo {repository_name}");
update_branches(&job_state, &mut local_repository)
.await
.log_err();
let t0 = Instant::now();
log::trace!("updating git statuses for repo {repository_name}");
let Some(statuses) = local_repository
.repo()
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
.log_err()
else {
return;
};
log::trace!(
"computed git statuses for repo {repository_name} in {:?}",
t0.elapsed()
);
let t0 = Instant::now();
let mut changed_paths = Vec::new();
let snapshot = job_state.lock().snapshot.snapshot.clone();
let Some(mut repository) = snapshot
.repository(local_repository.work_directory.path_key())
.context("Tried to update git statuses for a repository that isn't in the snapshot")
.log_err()
else {
return;
};
let merge_head_shas = local_repository.repo().merge_head_shas();
if merge_head_shas != local_repository.current_merge_head_shas {
mem::take(&mut repository.current_merge_conflicts);
}
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {
repo_path: repo_path.clone(),
status: *status,
},
&(),
);
if status.is_conflicted() {
repository.current_merge_conflicts.insert(repo_path.clone());
}
if let Some(path) = project_path {
changed_paths.push(path);
}
}
repository.statuses_by_path = new_entries_by_path;
let mut state = job_state.lock();
state
.snapshot
.repositories
.insert_or_replace(repository, &());
state
.snapshot
.git_repositories
.update(&local_repository.work_directory_id, |entry| {
entry.current_merge_head_shas = merge_head_shas;
entry.merge_message =
std::fs::read_to_string(local_repository.dot_git_dir_abs_path.join("MERGE_MSG"))
.ok()
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
entry.status_scan_id += 1;
});
util::extend_sorted(
&mut state.changed_paths,
changed_paths,
usize::MAX,
Ord::cmp,
);
log::trace!(
"applied git status updates for repo {repository_name} in {:?}",
t0.elapsed(),
);
tx.send(()).ok();
}
fn build_diff(
phase: BackgroundScannerPhase,
old_snapshot: &Snapshot,

View File

@@ -2426,11 +2426,10 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
});
}
// NOTE:
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
// a directory which some program has already open.
// This is a limitation of the Windows.
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
// you can't rename a directory which some program has already open. This is a
// limitation of the Windows. See:
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)]
async fn test_file_status(cx: &mut TestAppContext) {
@@ -3575,7 +3574,6 @@ fn git_checkout(name: &str, repo: &git2::Repository) {
repo.checkout_head(None).expect("Failed to check out head");
}
#[allow(dead_code)]
#[track_caller]
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
repo.statuses(None)

View File

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

View File

@@ -1 +1 @@
dev
stable

View File

@@ -383,14 +383,14 @@ impl Render for QuickActionBar {
"Column Git Blame",
show_git_blame_gutter,
IconPosition::Start,
Some(editor::actions::ToggleGitBlame.boxed_clone()),
Some(git::Blame.boxed_clone()),
{
let editor = editor.clone();
move |window, cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_git_blame(
&editor::actions::ToggleGitBlame,
&git::Blame,
window,
cx,
)