Compare commits

..

134 Commits

Author SHA1 Message Date
Joseph T. Lyons
e37aa89984 zed 0.177.8 2025-03-13 10:17:08 -04:00
gcp-cherry-pick-bot[bot]
81e0367d31 theme: Fix incorrect version control keys in One themes (cherry-pick #26606) (#26623)
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:44 -04:00
Nate Butler
8979aa8440 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:54:13 -04:00
Mikayla Maki
3f5bee3e23 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:11 -04:00
Conrad Irwin
ab6b368698 Fix overflow in create branch label (#26591)
Closes #ISSUE

Release Notes:

- N/A
2025-03-12 22:33:45 -04:00
Conrad Irwin
6d6998f0c8 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.
2025-03-12 22:27:55 -04:00
Alex Ozer
f0bc0bf007 Fix soft_wrap setting not applying to buffers starting with a different language (#25880)
Closes #22999 
# Problem

Currently, the default soft wrap mode of an editor is determined by
reading the language-specific settings of the language _at offset zero_
in the editor's (multi)buffer. While this provides a way to pick a
single soft wrap mode for a multi-language multibuffer, it's a bad
choice for a single-buffer multibuffer that begins with a different
embedded language. For example, Markdown with frontmatter:

```markdown
---
my_front_matter
---

# Hello World
```

Setting this in config:

```json
  "languages": {
    "Markdown": { "soft_wrap": "bounded" }
  },
```

Will not soft wrap the Markdown file as the language at offset zero is
YAML.

# Solution

Instead of using the language at offset zero, use the language of the
first buffer in the multibuffer (the buffer at offset zero). This gives
better behavior for single-buffer editors, and a similar default for
multi-language multibuffers as before.

# Testing

All existing `editor` crate tests pass, but I would appreciate any
guidance for where best to add additional testing.

Release Notes:

- Fixed soft_wrap setting not applying to buffers starting with a
different language

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-12 22:27:30 -04:00
Max Brunsfeld
3a681ecb5c Fix auto-indent when pasting multi-line content that was copied start… (#26246)
Closes https://github.com/zed-industries/zed/issues/24914 (again)

Release Notes:

- Fixed an issue where multi-line pasted content was auto-indented
incorrectly if copied from the middle of an existing line.
2025-03-12 22:02:48 -04:00
Peter Tripp
d0bd74be78 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:34 -04:00
Mikayla Maki
645d8ac58c Fix a bug where the modal layer could not be dismissed by the mouse 2025-03-12 16:46:17 -07:00
Peter Tripp
7f1ec5c221 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:34 -04:00
gcp-cherry-pick-bot[bot]
38cd1b853a Fix message on push (cherry-pick #26588) (#26594)
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:47:25 -04:00
gcp-cherry-pick-bot[bot]
438d4c7979 Fix unstage/stage in project diff not working when git panel isn't open (cherry-pick #26575) (#26580)
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:47 -06:00
gcp-cherry-pick-bot[bot]
b56b4723d4 Git on main thread (cherry-pick #26573) (#26585)
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 13:51:25 -06:00
Joseph T. Lyons
ab9b3f1ba3 v0.177.x stable 2025-03-12 12:52:45 -04:00
Julia Ryan
c20682336b Disable uncommit button for parentless commits (#25983)
Closes #25976

There's a couple states that this covers:
- upon `git init`, no footer is shown at all
- after 1 commit (or when on any parentless commit), the uncommit button
is ~disabled~ hidden
- otherwise commit button is shown

Also updated the button with "meta" tooltip showing human readable
description and git command.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2025-03-12 09:39:55 -07:00
Nate Butler
ca1ec68399 git_ui: Update Project Diff empty state design (#26554)
Title

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-12 12:24:07 -04:00
Danilo Leal
cf2a11ccde docs: Update the Git page (#26530)
So it reflects the new set of features supported starting from v0.177.

Release Notes:

- N/A
2025-03-12 12:23:59 -04:00
Mikayla Maki
06d9fbe675 git: Add an onboarding and banner flow (#26518)
TODO:

- [ ] Hide the reset onboarding action (only useful for development,
uncomment:
https://github.com/zed-industries/zed/pull/26518/files#diff-f0ce01d9a3df30f60c64b6f9906c54aa0191246a58dbf5297ee321575a180879R96)
- [x] Get a designer to replace the modal background (@danilo-leal)

Release Notes:

- Added a small onboarding banner for the git launch

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2025-03-12 12:21:13 -04:00
Nils Koch
f143d1b86e Check if additional git provider is not the original git provider (#26533)
Release Notes:

- N/A

Yesterday I worked on https://github.com/zed-industries/zed/pull/26482
and noticed afterwards that we have duplicated hosting providers if the
git remote host is "gitlab.com" and after the PR also for "github.com".
This is not a big problem, since the original providers are registered
first and therefore we first find a match with the original providers,
but I think we should address this nevertheless.

We initialize every hosting provider with the defaults here:

b008b2863e/crates/git_hosting_providers/src/git_hosting_providers.rs (L15-L24)

After that, we also register additional hosting providers:

b008b2863e/crates/git_hosting_providers/src/git_hosting_providers.rs (L30-L43)

If we do not check if the additional provider is not the original
provider, we will register the same provider twice.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-12 10:51:37 -04:00
Cole Miller
7728a8bffb git: Fix placeholder dots in untracked files (#26537)
This regressed at some point.

Release Notes:

- N/A
2025-03-12 10:18:54 -04:00
Nils Koch
d8bc10ec58 Add detection of self hosted GitHub enterprise instances (#26482)
This PR does not close an issue, but it is an issue and and fix in one.
I hope this is ok, but please let me know if you prefer me to open an
issue before.

Release Notes:

- Add "copy permalink" action for self-hosted GitHub enterprise
instances

# Issue
### Related issues:
* https://github.com/zed-industries/zed/issues/26393
* https://github.com/zed-industries/zed/issues/11043

When you try to copy a permalink from a self-hosted GitHub enterprise
instance, you get the following error:

<img width="383" alt="permalink"
src="https://github.com/user-attachments/assets/b32338a7-a2d7-48fc-86bf-ade1d32ed1f7"
/>

You also cannot open a PR or commit when you hover over a git blame:


https://github.com/user-attachments/assets/a5491ce7-270b-412f-b9ac-027ec020b028


### Reproduce
If you do not have access to a self-hosted GitHub instance, you can
change the remote url of any git repo:
```
git remote set-url origin git@github.mycorp.com:nilskch/zed.git
```

With the fix, permalinks still won't bring you to a valid website, but
you can verify that they are correctly created.

# Solution

Currently, we only support detecting self-hosted GitLab instances, but
not self-hosted GitHub instances. We detect GitLab instances by checking
if "gitlab" is part of the git URL.

This PR adds the same logic to detect self-hosted GitHub enterprise
instances (by checking if "github" is in the URL).

This solution is not ideal, since self-hosted GitHub or GitLab instances
might not contain the word "github" or "gitlab". #26393 proposes adding
a setting that would allow users to map specific domains to their
corresponding git provider types. This mapping would help Zed correctly
identify the appropriate git instance, even if "gitlab" or "github" are
not part of the URL.

This PR does not implement the offered solution, but I added a TODO
where the fix for #26393 has to make changes.
2025-03-12 09:47:38 -04:00
gcp-cherry-pick-bot[bot]
896276fc4a Don't clobber the user's upstream settings (cherry-pick #26486) (#26528)
Cherry-picked Don't clobber the user's upstream settings (#26486)

It's not clobbering time :(

Release Notes:

- Git Beta: Fixed a bug where our push button would always overwrite the
current branch's upstream

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-12 00:53:46 -07:00
Mikayla Maki
828dd3cf39 Add git init button (#26522)
Because why not

Release Notes:

- N/A
2025-03-12 00:42:09 -07:00
Julia Ryan
0bea86d065 Rework git toasts (#26420)
The notifications from git output could take up variable amounts of
screen space, and they were quite obnoxious when a git command printed
lots of output, such as fetching many new branches or verbose push
hooks.

This change makes the push/pull/fetch buttons trigger a small
notification toast, based on the output of the command that was ran. For
errors or commands with more output the user may want to see, there's an
"Open Log" button which opens a new buffer with the output of that
command.

It also uses this behavior for long error notifications for other git
commands like `commit` and `checkout`. The output of those commands can
be quite long due to arbitrary githooks running.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-12 00:35:46 -07:00
gcp-cherry-pick-bot[bot]
6a3204cf1d Hide generate commit message button when assistant is disabled (cherry-pick #26519) (#26521)
Cherry-picked Hide generate commit message button when assistant is
disabled (#26519)

Release Notes:

- Git Beta: Fixed the generate commit message button still showing when
the assistant is disabled.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-11 23:11:04 -07:00
gcp-cherry-pick-bot[bot]
6247405396 git: Remove hunk style setting (cherry-pick #26504) (#26517)
Cherry-picked git: Remove hunk style setting (#26504)

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 21:58:49 -07:00
gcp-cherry-pick-bot[bot]
4d521a6f71 git: Prevent up and down motions leaking out of the commit editor (cherry-pick #26501) (#26509) 2025-03-12 00:15:08 -04:00
gcp-cherry-pick-bot[bot]
a34e00e511 Fix git panel entries getting cut off (cherry-pick #26499) (#26500)
Cherry-picked Fix git panel entries getting cut off (#26499)

Closes #26497 

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 20:01:37 -04:00
gcp-cherry-pick-bot[bot]
6ef3870d5f git_ui: Truncate long repository and branch names for respective selectors in panel (cherry-pick #26483) (#26488)
Cherry-picked git_ui: Truncate long repository and branch names for
respective selectors in panel (#26483)

This PR fixes a long repo name pushing the branch selector off the
screen, as well as just generally truncating them down in a way smarter
than a fixed character limit when long.

| Before | After |
|---------|-----------|
| ![CleanShot 2025-03-11 at 17 21

31@2x](https://github.com/user-attachments/assets/8762b5a7-883c-4080-a6cf-e8007c4737e7)
| ![CleanShot 2025-03-11 at 17 21

44@2x](https://github.com/user-attachments/assets/c3904c29-d939-445f-b700-5bf73f257256)
|


Release Notes:

- Git Panel: Smart truncate long branch and repository names in their
respective selectors

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2025-03-11 16:14:30 -07:00
gcp-cherry-pick-bot[bot]
70247843db Fix conflict marker in project diff view (cherry-pick #26466) (#26492)
Cherry-picked Fix conflict marker in project diff view (#26466)

Closes #ISSUE

Release Notes:

- N/A

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

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-11 16:14:19 -07:00
gcp-cherry-pick-bot[bot]
c5c3c79a70 git_ui: Panel Horizontal Scroll (cherry-pick #26402) (#26496)
Cherry-picked git_ui: Panel Horizontal Scroll (#26402)

Known Issues:
- When items can horizontal scroll, the right selected border is hidden

TODO:
- [ ] Width calculation is off
- [ ] When scrollbars should autohide they don't until hovering the
panel
- [ ] When switching to and from scrollbar track being visible we are
missing a notify somewhere.

Release Notes:

- Git Panel: Added horizontal scrolling in the git panel

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 16:14:08 -07:00
gcp-cherry-pick-bot[bot]
bbea043d42 Git panel editor scroll (cherry-pick #26465) (#26493)
Cherry-picked Git panel editor scroll (#26465)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-11 16:14:00 -07:00
gcp-cherry-pick-bot[bot]
b7a242b22e Tweak stage/unstage-and-next to start a commit instead of wrapping in the project diff editor (cherry-pick #26434) (#26490)
Cherry-picked Tweak stage/unstage-and-next to start a commit instead of
wrapping in the project diff editor (#26434)

Release Notes:

- Git Beta: improved the stage-and-next and unstage-and-next actions in
the project diff editor to start a commit after acting on the last hunk

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 18:29:22 -04:00
Joseph T. Lyons
ac209f11f6 zed 0.177.7 2025-03-11 13:02:24 -04:00
gcp-cherry-pick-bot[bot]
44a43c72d0 Address out-of-bounds panic in inline completion button (cherry-pick #26394) (#26440)
Cherry-picked Address out-of-bounds panic in inline completion button
(#26394)

Closes #26350

Release Notes:

- Git Beta: Fixed a panic that could occur when using the project diff

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 11:54:25 -04:00
gcp-cherry-pick-bot[bot]
74c1ec5bac Show a disabled stage all button for no entries (cherry-pick #26436) (#26437)
Cherry-picked Show a disabled stage all button for no entries (#26436)

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-11 01:11:14 -04:00
gcp-cherry-pick-bot[bot]
61187a974e Fix enter binding in git panel's commit editor on Linux (cherry-pick #26427) (#26438)
Cherry-picked Fix enter binding in git panel's commit editor on Linux
(#26427)

Closes #26110 

Release Notes:

- Git Beta: fixed being unable to enter newline in the git panel's
commit editor on Linux

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-11 00:38:18 -04:00
Conrad Irwin
2da767d545 Go back to "create branch" in the list (#26433)
Closes #ISSUE

Release Notes:

- N/A
2025-03-10 22:26:51 -06:00
gcp-cherry-pick-bot[bot]
58bb9f344f Git commit modal branch list (cherry-pick #26417) (#26435)
Cherry-picked Git commit modal branch list (#26417)

Closes #26273

Release Notes:

- git: Fixes opening the branch selector in the commit modal with
cmd-option-b
- git: Truncates the branch selector in the commit modal

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 22:25:57 -06:00
gcp-cherry-pick-bot[bot]
8c3585d9b6 git_ui: Show more information in the branch picker (cherry-pick #25359) (#26432)
Cherry-picked git_ui: Show more information in the branch picker
(#25359)

Final product:

![CleanShot 2025-02-26 at 9  08

17@2x](https://github.com/user-attachments/assets/e5db1932-b2c6-4b32-ab67-ef0a0d19f022)

Release Notes:

- Added more information about Git branches to the branch picker.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Co-authored-by: Angelk90 <20476002+Angelk90@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 21:58:52 -06:00
gcp-cherry-pick-bot[bot]
1365247ff6 Use current upstream for permalink to line (cherry-pick #26398) (#26429)
Cherry-picked Use current upstream for permalink to line (#26398)

Release Notes:

- git: Copy permalink to line now uses the upstream of the current
branch instead of "origin"

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 21:49:34 -06:00
gcp-cherry-pick-bot[bot]
f4028ef9ad Unwind deprecated permalinks code (cherry-pick #26395) (#26426)
Cherry-picked Unwind deprecated permalinks code (#26395)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 20:54:06 -06:00
Max Brunsfeld
7fa916a488 Fix race conditions in updating buffer diffs on git changes (#26409)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-10 16:53:05 -07:00
gcp-cherry-pick-bot[bot]
198ecff6ad Fix panic opening branch picker in commit modal (cherry-pick #26407) (#26413)
Cherry-picked Fix panic opening branch picker in commit modal (#26407)

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 16:58:07 -06:00
gcp-cherry-pick-bot[bot]
a4b9b5df8b Git commit modal command (cherry-pick #26405) (#26411)
Cherry-picked Git commit modal command (#26405)

Fix KeyBinding::for_action() to use the active focus handle instead of
what was
rendered last.

This makes the UI consistently chose the cmd-escape binding for close
(because escape in the editor is editor::Cancel?),
so force it to be "escape"

Release Notes:

- git: Fixed escape tooltip in commit modal

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 16:36:31 -06:00
gcp-cherry-pick-bot[bot]
633df53388 Add a missing notify when updating the project diff (cherry-pick #26396) (#26400)
Cherry-picked Add a missing notify when updating the project diff
(#26396)

Closes #ISSUE

Release Notes:

- Git Beta: Fixed a bug that caused the project diff not to update in
response to git-related events

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

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-10 17:31:21 -04:00
Nate Butler
790fb9e5cb git_ui: Design Polish (#26361)
Polish PR

- [ ] Horizontal scrollbar for git panel
- [ ] Allow shift clicking a checkbox in any section to stage the whole
section
- [ ] Clean up design of no changes/pending push state in panel
- [x] Ensure checkbox placeholder dot is centered in the checkbox
- [x] Improve spacing between elements in git panel entries
- [x] Update git branch icon to match branch selector text when disabled
- [x] Truncate last commit message less aggressively in panel
- [x] Clean up new panel header design
- [x] Remove `_background` version control keys (backgrounds are derived
from the foreground colors)

Before:

![CleanShot 2025-03-10 at 11 54
32@2x](https://github.com/user-attachments/assets/46b18f66-bb5c-435e-a0da-6cc931bd8a15)

After:

![CleanShot 2025-03-10 at 11 55
24@2x](https://github.com/user-attachments/assets/fcf688c7-b949-41a2-a7b8-1a198eb7fa4a)

Before:

![CleanShot 2025-03-10 at 12 02
14@2x](https://github.com/user-attachments/assets/1990f4b3-c2f0-4e02-89ad-211aaebb3821)

After:

![CleanShot 2025-03-10 at 12 02
53@2x](https://github.com/user-attachments/assets/9b1caf65-c48f-44c9-924b-484892fb543f)

Release Notes:

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

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-10 17:27:51 -04:00
gcp-cherry-pick-bot[bot]
17c0113390 Remove GitUiFeatureFlag and enable panel unconditionally (cherry-pick #26386) (#26392)
Cherry-picked Remove GitUiFeatureFlag and enable panel unconditionally
(#26386)

Release Notes:

- git: Enable for everyone

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 14:50:16 -06:00
gcp-cherry-pick-bot[bot]
cb8d667cdb Scroll project diff into view always (cherry-pick #26379) (#26387)
Cherry-picked Scroll project diff into view always (#26379)

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 12:40:32 -06:00
Conrad Irwin
6d7943419b Git keyboard shortcuts (#26374)
Closes #26040

Release Notes:

- git: Add keyboard shortcuts (when the panel is open) for fetch `ctrl-g
ctrl-g`, pull `ctrl-g down`, push `ctrl-g up`, force-push `ctrl-g
shift-up`, open diff `ctrl-g d`
2025-03-10 12:28:45 -06:00
gcp-cherry-pick-bot[bot]
d474438c9d Restore git panel header (cherry-pick #26354) (#26380)
Cherry-picked Restore git panel header (#26354)

Let's play around with it. This should not be added to tomorrow's
preview.

Release Notes:

- Git Beta: Added a panel header with an open diff and stage/unstage all
buttons.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-10 12:27:59 -06:00
gcp-cherry-pick-bot[bot]
c21ab404e5 git: Pass project environment to git binary invocations (cherry-pick #26301) (#26373)
Cherry-picked git: Pass project environment to git binary invocations
(#26301)

Closes #26213 

Release Notes:

- Git Beta: pass down environment variables from project to git
operations

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-10 12:31:30 -04:00
Joseph T. Lyons
4b24ef3d7e zed 0.177.6 2025-03-10 12:31:01 -04:00
João Marcos
1d7ffff974 Remove redundant checks in do_stage_or_unstage_and_next (#26364)
Release Notes:

- N/A
2025-03-10 12:26:47 -04:00
gcp-cherry-pick-bot[bot]
44c93b087f git_ui: Branch picker improvements (cherry-pick #26287) (#26371)
Cherry-picked git_ui: Branch picker improvements (#26287)

- Truncate branch names based on the width of the picker
- Use a footer for "Create branch" instead of a picker entry

Still to do:

- [x] Select the footer button when no matches and run the create logic
on `enter`
- [x] Make it possible to quickly select the footer button from the
keyboard when there are matches

Release Notes:

- Git Beta: Removed limitation that made it impossible to create a
branch from the branch picker when it too closely resembled an existing
branch name

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-10 12:12:36 -04:00
Richard Hao
4144a2e37c git: Disable commit message generation when commit not possible (#26329)
## Issue:

- `Generate Commit Message` will generate a random message if there are
no changes.
<img width="614" alt="image"
src="https://github.com/user-attachments/assets/c16cadac-01af-47c0-a2db-a5bbf62f84bb"
/>


## After Fixed:

- `Generate Commit Message` will be disabled if commit is not possible
<img width="610" alt="image"
src="https://github.com/user-attachments/assets/5ea9ca70-6fa3-4144-ab4e-be7a986d5496"
/>


## Release Notes:

- Fixed: Disable commit message generation when commit is not possible
2025-03-10 02:49:24 -04:00
Max Brunsfeld
1eff87c2f6 Follow-up fixes for recent multi buffer optimizations (#26345)
I realized that the optimization broke multi buffer syncing after buffer
reparses.

Release Notes:

- N/A
2025-03-09 22:16:28 -07:00
Danilo Leal
25997137fc git: Refine diff hunk controls visuals (#26317)
You may need to zoom in hard to see this 😅 but the main addition of this
PR is just ensuring there's also horizontal border instead of just on
the bottom. Also added some box-shadow here to make it pop out of the
diff a bit more.

| Before | After |
|--------|--------|
| ![CleanShot 2025-03-08 at 12  37
40@2x](https://github.com/user-attachments/assets/98e0329e-646f-4455-89fa-3f6ec1211361)
| ![CleanShot 2025-03-08 at 12  36
40@2x](https://github.com/user-attachments/assets/7f667e65-5b72-4156-b0ec-2b162eb76f2f)
|

Release Notes:

- N/A
2025-03-10 00:15:58 -04:00
Max Brunsfeld
ea2df99770 Fix performance bottlenecks when multi buffers have huge numbers of buffers (#26308)
This is motivated by trying to make the Project Diff view usable with
huge Git change sets.

Release Notes:

- Improved performance of rendering multibuffers with very large numbers
of buffers
2025-03-10 00:15:58 -04:00
Mikayla Maki
6c574fa94c Git UI papercuts (#26316)
Release Notes:

- Git Beta: added `git:Add` as an alias for the existing `git::Diff`
- Git Beta: Fixed a bug where the 'generate commit message' keybinding
wasn't working.
- Git Beta: Made the empty project diff state a little more helpful with
a button to push, and a button to close the item.
2025-03-10 00:15:58 -04:00
Mikayla Maki
783e239d52 Add git branch switching aliases (#26315)
This gives us _very_ rudimentary support for `git switch` and `git
checkout` now, by making them aliases for our existing `git::branch`
call.

Release Notes:

- Git Beta: Added `git::Switch` and `git::CheckoutBranch` as aliases for
the existing `git::Branch`
2025-03-10 00:09:38 -04:00
Mikayla Maki
4c455ca96e Improve when the commit suggestions would show (#26313)
Release Notes:

- Git Beta: Fixed a few bugs where the suggested commit text wouldn't
show in certain cases, or would update slowly.
2025-03-10 00:08:59 -04:00
Kiran_Peraka
270e47cc79 git: Fix errors not showing in the toast notification (#26303)
Release Notes:

- Resolved an issue where error messages from Git were not being
displayed in toast notifications.
<img width="1702" alt="Screenshot 2025-03-08 at 1 11 30 AM"
src="https://github.com/user-attachments/assets/a46517db-4e64-4c5e-a64e-96e820ca9aec"
/>
2025-03-10 00:08:50 -04:00
Mikayla Maki
c075dd5bd6 Change the default staging and unstaging state display (#26299)
This adds a setting for the "border" hunk display mode, as discussed,
and makes it the default.

Here's how it looks in light mode:

<img width="1512" alt="Screenshot 2025-03-07 at 11 39 25 AM"
src="https://github.com/user-attachments/assets/a934faa3-ec69-47e1-ad46-535e48b98e9f"
/>

And dark mode:

<img width="1511" alt="Screenshot 2025-03-07 at 11 39 56 AM"
src="https://github.com/user-attachments/assets/43c9afd1-22bb-4bd8-96ce-82702a6cbc80"
/>

Release Notes:

- Git Beta: Adjusted the default hunk styling for staged and unstaged
changes

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nate <nate@zed.dev>
2025-03-10 00:08:27 -04:00
Conrad Irwin
3129e3dcf5 Remove worktree and project notifies (#26244)
This reduces the number of multibuffer syncs from 100,000 to 20,000.
Before this change each editor individually observed the project, so
literally any project change was amplified by the number of editors you
had open.

Now editors listen to their buffers instead of the project, and other
users of `cx.observe` on the project have been updated to use specific
events to reduce churn.

Follow up to #26237

Release Notes:

- Improved performance of Zed in large repos with lots of file system
events.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
(cherry picked from commit 80fb88520f)
2025-03-10 00:06:01 -04:00
Conrad Irwin
dfbfcdfa2c Track dirtyness per item (#26237)
This reduces the number of multibuffer syncs when starting the editor
with 80
files open in the Zed repo from 10,000,000 to 100,000 by avoiding
O(n**2)
dirtyness checks.

Release Notes:

- Fixed a beachball when restarting in a large repo with a large number
open files
2025-03-10 00:06:01 -04:00
gcp-cherry-pick-bot[bot]
53ff7ca27d worktree: Add some info-level logging about added and removed repository entries (cherry-pick #26291) (#26292)
Cherry-picked worktree: Add some info-level logging about added and
removed repository entries (#26291)

Trying to track down a user's reported issue with parent repositories
not getting picked up.

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-07 14:02:36 -05:00
Joseph T. Lyons
0a391abaa4 zed 0.177.5 2025-03-07 13:55:22 -05:00
Marshall Bowers
e29fb616ed git_ui: Try to prompt the model out of including the diff output (#26281)
This PR updates the prompt for generating commit messages to tell the
model not to include the raw diff output in the message.

Release Notes:

- N/A
2025-03-07 13:50:30 -05:00
Kirill Bulatov
9db97b887c Fix inline diagnostics in the project diff (#26275)
205f9a9f03/crates/editor/src/element.rs (L1643)

Due to the snippet above, Zed is supposed to have `row` larger or equal
to `start_row` here:


205f9a9f03/crates/editor/src/element.rs (L1694)

yet the panic were reported when clicking in the project diff.

That project diff has a lot of highlighting happening already, so the PR
disables inline diagnostics within a git diff view.


Release Notes:

- N/A
2025-03-07 13:50:30 -05:00
loczek
871bca5b4d git: Fix git panel not using default width (#26220)
Closes #26062

Removing the width here causes zed to use the default value (inside
default settings) after restart like other panels.

Release Notes:

- Fixed issue where git panel wasn't using default width after restart

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-07 13:50:30 -05:00
Mikayla Maki
6dde040b97 Add staged variants of the hunk_style controls (#26259)
This PR adds a few more hunk style settings that flips the emphasis.
Normally, the concept at Zed has been that the project diff should
emphasize what's going into the commit. However, this leads to a problem
where the default state of all diff hunks are in the non-emphasized
state, making them hard to see and interact with. Especially on light
themes. This PR is an experiment in flipping the emphasis states. Now
the project diff is more like a queue of work, with the next "job" (hunk
to be evaluated) emphasized, and the "completed" (staged) hunks
deemphasized. This fixes the default state issue but is a big jump from
how we've been thinking about it. So here we can try it out and see how
it feels :)

Release Notes:

- Git Beta: Added hunk style settings to emphasize the unstaged state,
rather than the staged state.
2025-03-07 13:49:32 -05:00
Mikayla Maki
d6190dbe4f Restore co-author hiding (#26257)
Release Notes:

- N/A
2025-03-07 13:49:32 -05:00
Marshall Bowers
7365cac495 Improve the generate commit message design (#26233)
[WIP]

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-07 13:49:32 -05:00
Cole Miller
84f8b86fb9 Fix paths on Windows in new test (#26255)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-07 13:49:32 -05:00
Cole Miller
e53389fd53 Disable restore hunk control for created files (#25841)
Release Notes:

- Git Beta: disable hunk restore action and button for created files
2025-03-07 13:49:32 -05:00
Nate Butler
12fb23bf62 Add StatusToast & the ToastLayer (#26232)
https://github.com/user-attachments/assets/b16e32e6-46c6-41dc-ab68-1824d288c8c2

This PR adds the first part of our planned extended notification system:
StatusToasts.

It also makes various updates to ComponentPreview and adds a `Styled`
extension in `ui::style::animation` to make it easier to animate styled
elements.

_**Note**: We will be very, very selective with what elements are
allowed to be animated in Zed. Assume PRs adding animation to elements
will all need to be manually signed off on by a designer._

## Status Toast

![CleanShot 2025-03-06 at 14 15
52@2x](https://github.com/user-attachments/assets/b65d4661-f8d1-4e98-b9be-2c05cba1409f)

These are designed to be used for notifying about things that don't
require an action to be taken or don't need to be triaged. They are
designed to be ignorable, and dismiss themselves automatically after a
set time.

They can optionally include a single action. 

Example: When the user enables Vim Mode, that action might let them undo
enabling it.

![CleanShot 2025-03-06 at 14 18
34@2x](https://github.com/user-attachments/assets/eb6cb20e-c968-4f03-88a5-ecb6a8809150)

Status Toasts should _not_ be used when an action is required, or for
any binary choice.

If the user must provide some input, this isn't the right component!

### Out of scope

- Toasts should fade over a short time (like AnimationDuration::Fast or
Instant) when dismissed
- We should visually show when the toast will dismiss. We'll need to
pipe the `duration_remaining` from the toast layer -> ActiveToast to do
this.
- Dismiss any active toast if another notification kind is created, like
a Notification or Alert.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-07 13:14:44 -05:00
Joseph T. Lyons
75f7def31c Add event to track LLM-generated commit messages (#26245)
Release Notes:

- N/A
2025-03-07 13:13:51 -05:00
Marshall Bowers
9195cd5233 git_ui: Add support for generating commit messages with an LLM (#26227)
This PR finishes up the support for generating commit messages using an
LLM.

We're shelling out to `git diff` to get the diff text, as it seemed more
efficient than attempting to reconstruct the diff ourselves from our
internal Git state.


https://github.com/user-attachments/assets/9bcf30a7-7a08-4f49-a753-72a5d954bddd

Release Notes:

- Git Beta: Added support for generating commit messages using a
language model.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-07 13:13:17 -05:00
Marshall Bowers
f0be14277a git_ui: Scaffold out support for generating commit messages with an LLM (#26161)
This PR adds the rough structure needed to support generating commit
messages using an LLM.

This functionality is not yet surfaced to the user.

This is the current state, if you tweak the source to show the button:

https://github.com/user-attachments/assets/66d1fbc4-09f3-4277-84f4-e9c9ebab274c

Release Notes:

- N/A
2025-03-07 13:12:46 -05:00
Cole Miller
0dec6fbfab Cherry-pick: Worktree paths in git panel, take 2 (#26047) (#26252)
Release Notes:

- Git Beta: updated the git panel to use worktree-relative paths and
support opening files outside the active repository's worktree
2025-03-06 19:26:27 -05:00
Max Brunsfeld
4f8a094c11 Fix early return when reaching end excerpt in lift_buffer_metadata (#26253)
Release Notes:

- Fixed a bug causing slowness when viewing multi buffers with lots of
excerpts
2025-03-06 16:26:15 -08:00
gcp-cherry-pick-bot[bot]
43c81067c7 Disable restore hunk control for created files (cherry-pick #25841) (#26251)
Cherry-picked Disable restore hunk control for created files (#25841)

Release Notes:

- Git Beta: disable hunk restore action and button for created files

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-06 18:51:36 -05:00
gcp-cherry-pick-bot[bot]
b7b700e4f7 language_models: Remove .unwraps in Bedrock provider (cherry-pick #26238) (#26241)
Cherry-picked language_models: Remove `.unwrap`s in Bedrock provider
(#26238)

This PR removes a number of `.unwrap`s in the Bedrock provider.

We must not `.unwrap` in situations where it is not provably safe to do
so, which it was not in any of these cases.

Release Notes:

- Fixed some potential panics in the AWS Bedrock model provider.

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-06 16:18:28 -05:00
gcp-cherry-pick-bot[bot]
f3fb7b776d Git telemetry (cherry-pick #26222) (#26229)
Cherry-picked Git telemetry (#26222)

Release Notes:

- git: Adds telemetry to git actions

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-06 11:40:53 -07:00
Conrad Irwin
decfd9877a Git askpass (#25953)
Supersedes #25848

Release Notes:

- git: Supporting push/pull/fetch when remote requires auth

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-06 10:46:00 -07:00
Max Brunsfeld
661e36f736 Clear pending staged/unstaged diff hunks hunks when writing to the git index fails (#26173)
Release Notes:

- Git Beta: Fixed a bug where discarding a hunk in the project diff view
performed two concurrent saves of the buffer.
- Git Beta: Fixed an issue where diff hunks appeared in the wrong state
after failing to write to the git index.
2025-03-06 10:45:52 -07:00
Julia Ryan
c80d25174f Git uncommit warning (#25977)
Adds a prompt when clicking the uncommit button when the current commit
is already present on a remote branch:

![screenshot showing
prompt](https://github.com/user-attachments/assets/d6421875-588e-4db0-aee0-a92f36bce94b)

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2025-03-06 10:44:54 -07:00
Peter Tripp
420bb84152 ci: Less Windows CI for PRs (#26155)
Split Windows GHA CI job into `windows_clippy` and `windows_tests`
(`cargo test` and `cargo build`). `windows_clippy` will continue to run
on every PR commit, but `windows_tests` will only be run on main. Tag a
PR `windows` if you would like to run windows tests.

Added a call to the Azure metadata service to detect the Azure hardware
used by the GitHub hosted Windows runners. This is temporary and I'll
remove once I've gathered some data (adds 5-15secs to Windows CI times)

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-06 10:43:14 -07:00
Conrad Irwin
b8d1c3c866 git: Make repo selector wider (#26149)
…m_item()

Closes #ISSUE

Release Notes:

- git: Fixed repository selector being too narrow
2025-03-06 10:41:36 -07:00
Mikayla Maki
090c38d872 Fix git branches in non-active repository (#26148)
Release Notes:

- Git Beta: Fixed a bug where the branch selector would only show for
the first repository opened.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-03-06 10:41:15 -07:00
Joseph T. Lyons
8a0fb9100e zed 0.177.4 2025-03-06 11:06:23 -05:00
0x2CA
664ccc48c8 git: Fix git commit font fallbacks (#26184)
Closes #ISSUE

Release Notes:

- Fixed git commit font_fallbacks
2025-03-06 10:49:57 -05:00
gcp-cherry-pick-bot[bot]
b575bc9a9d Fix panic in commit editor selections syncing (cherry-pick #26186) (#26188)
Cherry-picked Fix panic in commit editor selections syncing (#26186)

Closes #26183 

Release Notes:

- Git Beta: Fixed a panic when selecting text in one of the commit
message editors

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-05 22:43:13 -05:00
gcp-cherry-pick-bot[bot]
da895a6fd8 git: Add CHERRY_PICK_HEAD to the list of merge heads (cherry-pick #26145) (#26168)
Cherry-picked git: Add CHERRY_PICK_HEAD to the list of merge heads
(#26145)

Attempt to fix an issue where conflicts from a cherry-pick don't get
cleared out of the git panel after being resolved.

Release Notes:

- Git Beta: Fixed resolution of conflicts from cherry-picks not being
reflected in the git panel

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-05 21:26:21 -05:00
gcp-cherry-pick-bot[bot]
2309721274 Fix panic when expanding a deletion hunk with blame open (cherry-pick #26130) (#26142)
Cherry-picked Fix panic when expanding a deletion hunk with blame open
(#26130)

Closes #26118

Release Notes:

- Fixed a panic when expanding diff hunks while git blame is open

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-05 13:17:32 -05:00
Marshall Bowers
fa2f982848 Fix language model selector (#26138)
This PR fixes the language model selector.

I tried to piece together the state prior to #25697 (the state it was in
at 11838cf89e) while retaining unrelated
changes that happened since then.

Release Notes:

- Fixed an issue where language models would not be authenticated until
after the model selector was opened (Preview only).
2025-03-05 12:49:42 -05:00
Marshall Bowers
78b460f701 Clean up language model selector (#26134)
This PR does some cleanup for the language model selector after
https://github.com/zed-industries/zed/pull/26090.

Release Notes:

- N/A
2025-03-05 12:49:34 -05:00
Cole Miller
2d5063b5f5 zed 0.177.3 2025-03-05 12:47:49 -05:00
Max Brunsfeld
a87929c5fd Bump Tree-sitter to 0.25.3 for error recovery fixes (#26092)
For https://github.com/tree-sitter/tree-sitter/pull/4257

Release Notes:

- Fixed a hang that could occur when editing certain Zig files.
2025-03-05 09:44:59 -08:00
gcp-cherry-pick-bot[bot]
535c949a1a Fix performance regression in multibuffer diff syncing (cherry-pick #26137) (#26139)
Cherry-picked Fix performance regression in multibuffer diff syncing
(#26137)

This fixes a performance problem introduced in #25906 and caused by
calling `BufferDiff::snapshot` too frequently.

Release Notes:

- Fixed a performance regression related to buffer diffs

Co-authored-by: Conrad <conrad@zed.dev>

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
2025-03-05 12:41:37 -05:00
Joseph T. Lyons
4b6fcef379 zed 0.177.2 2025-03-05 08:51:18 -05:00
gcp-cherry-pick-bot[bot]
dc374713d8 Fix diff_hunk_before in a multibuffer (cherry-pick #26059) (#26102)
Cherry-picked Fix diff_hunk_before in a multibuffer (#26059)

Also simplify it to avoid doing a bunch of unnecessary work.

Co-Authored-By: Cole <cole@zed.dev>

Closes #ISSUE

Release Notes:

- git: Fix jumping to the previous diff hunk

---------

Co-authored-by: Cole <cole@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Cole <cole@zed.dev>
2025-03-04 21:45:57 -07:00
gcp-cherry-pick-bot[bot]
c084706377 git: Improvements to fetch/push/pull (cherry-pick #26041) (#26051)
Cherry-picked git: Improvements to fetch/push/pull (#26041)

- Add global handlers so these actions can be invoked from the command
palette, etc.
- Tweak spinner to not show itself until a remote has been selected

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-04 20:09:43 -07:00
gcp-cherry-pick-bot[bot]
578c9f826b Synchronize modal commit editor with panel editor (cherry-pick #26068) (#26093)
Cherry-picked Synchronize modal commit editor with panel editor (#26068)

Release Notes:

- Git Beta: Synchronized selections between the modal editor and the
panel editor
- Git Beta: Allow opening the commit modal even if we're unable to
commit.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-03-04 20:07:06 -07:00
gcp-cherry-pick-bot[bot]
f06cee40df Fix focus handle leak (cherry-pick #26090) (#26094)
Cherry-picked Fix focus handle leak (#26090)

This fixes a major performance issue in the current git beta.
This PR also removes the PopoverButton component, which was easy to
misuse.

Release Notes:

- Git Beta: Fix frame drops caused by opening the git panel

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-03-04 20:06:55 -07:00
Max Brunsfeld
1f936eccc7 Fix lag when large diff hunk intersects the viewport (#26088)
We were iterating over the row range of a hunk, and inserting into a
hash map for every row.

Release Notes:

- Fixed a performance problem when a large diff hunk was displayed in an
editor.
2025-03-04 16:28:56 -08:00
Joseph T. Lyons
1516ee3e46 zed 0.177.1 2025-03-04 13:04:11 -05:00
gcp-cherry-pick-bot[bot]
53af68aa82 git: Fix project diff shortcuts (cherry-pick #26045) (#26049)
Cherry-picked git: Fix project diff shortcuts (#26045)

Release Notes:

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

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

Improve parsing of git remote outputs

Release Notes:

- N/A

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

Release Notes:

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

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

Release Notes:

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

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

It has 2 options:

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

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

Transparent:

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

Pattern:

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

Release Notes:

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

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

Release Notes:

- N/A

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

Release Notes:

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

Release Notes:

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

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



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

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



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

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



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

Things work without flickering:



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


Release Notes:

- Fixed a panic on Linux theme appearance change

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

Release Notes:

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

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

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



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

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

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

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

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

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


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

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

Release Notes:

- Improved `.git` scan heuristics

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

Also:

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

Release Notes:

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

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

Closes #25951

Release Notes:

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

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

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

Release Notes:

- N/A

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

Release Notes:

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

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

Closes #24243

Release Notes:

- vim: Fix j/k on folded multibuffer headers

---------

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

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

Updates #25835
Updates #25951

Closes #ISSUE

Release Notes:

- Fixed toggling folds from within deleted hunks

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

Release Notes:

- N/A

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

Release Notes:

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

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

**Before**

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

**After**

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


Closes #25955

Release Notes:

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

Release Notes:

- N/A

---------

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

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

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

Release Notes:

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

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

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

Release Notes:

- N/A

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

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-03-03 14:25:05 -05:00
Joseph T. Lyons
3130b46515 v0.177.x preview 2025-03-03 12:42:43 -05:00
329 changed files with 11075 additions and 11118 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,12 @@ 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'
runs-on: windows-2025-64
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 +419,44 @@ 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() && (
needs.style.result == 'success'
&& (
needs.job_spec.outputs.run_tests == 'false'
|| (needs.macos_tests.result == 'success'
&& needs.linux_tests.result == 'success'
&& needs.windows_tests.result == 'success'
&& needs.windows_clippy.result == 'success'
&& needs.build_remote_server.result == 'success'
&& needs.migration_checks.result == 'success')
)
)
steps:
- name: All tests passed
run: echo "All tests passed successfully!"
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 +546,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 +565,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 +575,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 +607,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 +626,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 +636,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 +665,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

@@ -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/

1706
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -118,7 +118,6 @@ members = [
"crates/rope",
"crates/rpc",
"crates/schema_generator",
"crates/scripting_tool",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
@@ -169,10 +168,15 @@ members = [
# Extensions
#
"extensions/csharp",
"extensions/deno",
"extensions/elixir",
"extensions/emmet",
"extensions/erlang",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
"extensions/lua",
"extensions/perplexity",
"extensions/proto",
"extensions/purescript",
@@ -319,7 +323,6 @@ reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
scripting_tool = { path = "crates/scripting_tool" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
@@ -452,7 +455,6 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored"] }
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
@@ -601,11 +603,9 @@ features = [
version = "0.58"
features = [
"implement",
"Foundation_Collections",
"Foundation_Numerics",
"Storage",
"System_Threading",
"UI_StartScreen",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",

View File

@@ -0,0 +1,40 @@
<svg width="400" height="120" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="tilePattern" width="124" height="24" patternUnits="userSpaceOnUse">
<svg width="124" height="24" viewBox="0 0 124 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.2">
<path d="M16.666 12.0013L11.9993 16.668L7.33268 12.0013" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7.33464L12 16.668" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 8.33464C29.3682 8.33464 29.6667 8.03616 29.6667 7.66797C29.6667 7.29978 29.3682 7.0013 29 7.0013C28.6318 7.0013 28.3333 7.29978 28.3333 7.66797C28.3333 8.03616 28.6318 8.33464 29 8.33464ZM29 9.66797C30.1046 9.66797 31 8.77254 31 7.66797C31 6.5634 30.1046 5.66797 29 5.66797C27.8954 5.66797 27 6.5634 27 7.66797C27 8.77254 27.8954 9.66797 29 9.66797Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35 8.33464C35.3682 8.33464 35.6667 8.03616 35.6667 7.66797C35.6667 7.29978 35.3682 7.0013 35 7.0013C34.6318 7.0013 34.3333 7.29978 34.3333 7.66797C34.3333 8.03616 34.6318 8.33464 35 8.33464ZM35 9.66797C36.1046 9.66797 37 8.77254 37 7.66797C37 6.5634 36.1046 5.66797 35 5.66797C33.8954 5.66797 33 6.5634 33 7.66797C33 8.77254 33.8954 9.66797 35 9.66797Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 16.9987C29.3682 16.9987 29.6667 16.7002 29.6667 16.332C29.6667 15.9638 29.3682 15.6654 29 15.6654C28.6318 15.6654 28.3333 15.9638 28.3333 16.332C28.3333 16.7002 28.6318 16.9987 29 16.9987ZM29 18.332C30.1046 18.332 31 17.4366 31 16.332C31 15.2275 30.1046 14.332 29 14.332C27.8954 14.332 27 15.2275 27 16.332C27 17.4366 27.8954 18.332 29 18.332Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.334 9H29.6673V11.4615C30.2383 11.1443 31.0005 11 32.0007 11H33.6675C34.0356 11 34.334 10.7017 34.334 10.3333V9H35.6673V10.3333C35.6673 11.4378 34.7723 12.3333 33.6675 12.3333H32.0007C30.8614 12.3333 30.3692 12.5484 30.1298 12.7549C29.9016 12.9516 29.7857 13.2347 29.6673 13.742V15H28.334V9Z" fill="white"/>
<path d="M48.668 8.66406H55.3346V15.3307" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M48.668 15.3307L55.3346 8.66406" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M76.5871 9.40624C76.8514 9.14195 77 8.78346 77 8.40965C77 8.03583 76.8516 7.67731 76.5873 7.41295C76.323 7.14859 75.9645 7.00005 75.5907 7C75.2169 6.99995 74.8584 7.14841 74.594 7.4127L67.921 14.0874C67.8049 14.2031 67.719 14.3456 67.671 14.5024L67.0105 16.6784C66.9975 16.7217 66.9966 16.7676 67.0076 16.8113C67.0187 16.8551 67.0414 16.895 67.0734 16.9269C67.1053 16.9588 67.1453 16.9815 67.1891 16.9925C67.2328 17.0035 67.2788 17.0024 67.322 16.9894L69.4985 16.3294C69.6551 16.2818 69.7976 16.1964 69.9135 16.0809L76.5871 9.40624Z" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74 8L76 10" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M70.3877 7.53516V6.53516" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M73.5693 16.6992V17.6992" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M66.3877 10.5352H67.3877" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M77.5693 13.6992H76.5693" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M68.3877 8.53516L67.3877 7.53516" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M75.5693 15.6992L76.5693 16.6992" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M87.334 11.9987L92.0007 7.33203L96.6673 11.9987" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M92 16.6654V7.33203" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M117 12C117 10.6739 116.473 9.40215 115.536 8.46447C114.598 7.52678 113.326 7 112 7C110.602 7.00526 109.261 7.55068 108.256 8.52222L107 9.77778" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M107 7V9.77778H109.778" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M107 12C107 13.3261 107.527 14.5979 108.464 15.5355C109.402 16.4732 110.674 17 112 17C113.398 16.9947 114.739 16.4493 115.744 15.4778L117 14.2222" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M114.223 14.2188H117V16.9965" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
</pattern>
<linearGradient id="fade" y2="1" x2="0">
<stop offset="0" stop-color="white" stop-opacity=".52"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<mask id="fadeMask" maskContentUnits="objectBoundingBox">
<rect width="1" height="1" fill="url(#fade)"/>
</mask>
</defs>
<rect width="100%" height="100%" fill="url(#tilePattern)" mask="url(#fadeMask)"/>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,5 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 296 B

View File

@@ -393,6 +393,7 @@
"alt-shift-open": "projects::OpenRemote",
"alt-ctrl-shift-o": "projects::OpenRemote",
"alt-ctrl-shift-b": "branches::OpenRecent",
"alt-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
@@ -612,29 +613,12 @@
"ctrl-alt-e": "assistant2::RemoveAllContext"
}
},
{
"context": "AssistantPanel2 && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewPromptEditor",
"cmd-alt-t": "assistant2::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "assistant2::Chat"
}
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
@@ -733,28 +717,48 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"alt-shift-y": "git::UnstageFile",
"ctrl-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm"
"alt-enter": "menu::SecondaryConfirm",
"backspace": "git::RestoreFile"
}
},
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
},
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit"
"ctrl-enter": "git::Commit",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
},
{
@@ -769,6 +773,7 @@
"escape": "git_panel::FocusChanges",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"

View File

@@ -31,13 +31,13 @@
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-escape": "menu::Cancel",
"cmd-o": "workspace::Open",
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -256,14 +256,6 @@
"cmd-alt-e": "assistant2::RemoveAllContext"
}
},
{
"context": "AssistantPanel2 && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewPromptEditor",
"cmd-alt-t": "assistant2::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
@@ -271,15 +263,6 @@
"enter": "assistant2::Chat"
}
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
@@ -504,6 +487,7 @@
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
"alt-shift-enter": "toast::RunAction",
"cmd-shift-s": "workspace::SaveAs",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
@@ -753,28 +737,25 @@
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"cmd-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"cmd-shift-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"cmd-y": "git::StageFile",
"cmd-shift-y": "git::UnstageFile",
"alt-down": "git_panel::FocusEditor",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit"
"cmd-enter": "git::Commit",
"backspace": "git::RestoreFile"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit"
}
},
{
"context": "AskPass > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
"cmd-enter": "git::Commit",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
},
{
@@ -790,11 +771,27 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
"alt-tab": "git::GenerateCommitMessage"
}

View File

@@ -6,7 +6,7 @@
"a": ["vim::PushObject", { "around": true }],
"left": "vim::Left",
"h": "vim::Left",
"backspace": "vim::WrappingLeft",
"backspace": "vim::Backspace",
"down": "vim::Down",
"ctrl-j": "vim::Down",
"j": "vim::Down",
@@ -20,7 +20,7 @@
"k": "vim::Up",
"right": "vim::Right",
"l": "vim::Right",
"space": "vim::WrappingRight",
"space": "vim::Space",
"end": "vim::EndOfLine",
"$": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
@@ -247,8 +247,7 @@
"context": "VimControl && VimCount",
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand",
"%": "vim::GoToPercentage"
":": "vim::CountCommand"
}
},
{

View File

@@ -555,6 +555,12 @@
//
// Default: icon
"status_style": "icon",
// What branch name to use if init.defaultBranch
// is not set
//
// Default: main
"fallback_branch_name": "main",
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -720,8 +726,8 @@
"remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
//
@@ -837,15 +843,7 @@
//
// The minimum column number to show the inline blame information at
// "min_column": 0
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
// 1. Show unstaged hunks with a transparent background (default):
// "hunk_style": "transparent"
// 2. Show unstaged hunks with a pattern background:
// "hunk_style": "pattern"
"hunk_style": "transparent"
}
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.
@@ -1175,7 +1173,6 @@
"format_on_save": "off",
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "bounded",
"prettier": {
"allowed": true
}
@@ -1299,11 +1296,6 @@
// "semi": false,
// "singleQuote": true
},
// Settings for auto-closing of JSX tags.
"jsx_tag_auto_close": {
// // Whether to auto-close JSX tags.
// "enabled": true
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.

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",
@@ -394,15 +386,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",
@@ -493,9 +477,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",
@@ -782,15 +766,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",
@@ -881,9 +857,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",
@@ -1170,15 +1146,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",
@@ -1269,9 +1237,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",
@@ -1558,15 +1526,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",
@@ -1657,9 +1617,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",
@@ -1946,15 +1906,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",
@@ -2045,9 +1997,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

@@ -9,7 +9,10 @@ use gpui::{
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent, WorktreeId,
};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
@@ -73,7 +76,22 @@ impl ActivityIndicator {
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
_ => {}
},
)
.detach();
cx.subscribe(
&project.read(cx).environment().clone(),
|_, _, event, cx| match event {
ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
},
)
.detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
@@ -204,7 +222,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.remove_environment_error(cx, worktree_id);
project.remove_environment_error(worktree_id, cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),

View File

@@ -110,7 +110,7 @@ impl ConfigurationView {
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_sm()
.rounded_md()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",

View File

@@ -38,7 +38,7 @@ use language_model::{
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{ActionVariant, CodeAction, ProjectTransaction};
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use rope::Rope;
use settings::{update_settings_file, Settings, SettingsStore};
@@ -3569,10 +3569,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
lsp_action: lsp::CodeAction {
title: "Fix with Assistant".into(),
..Default::default()
})),
},
}]))
} else {
Task::ready(Ok(Vec::new()))

View File

@@ -2,19 +2,17 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length,
ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement,
UnderlineStyle, WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use language::LanguageRegistry;
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, KeyBinding};
use util::ResultExt as _;
use ui::{prelude::*, Disclosure};
use workspace::Workspace;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
@@ -28,20 +26,14 @@ pub struct ActiveThread {
tools: Arc<ToolWorkingSet>,
thread_store: Entity<ThreadStore>,
thread: Entity<Thread>,
save_thread_task: Option<Task<()>>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
struct EditMessageState {
editor: Entity<Editor>,
}
impl ActiveThread {
pub fn new(
thread: Entity<Thread>,
@@ -63,18 +55,16 @@ impl ActiveThread {
tools,
thread_store,
thread: thread.clone(),
save_thread_task: None,
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
expanded_tool_uses: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, window, cx))
move |ix, _: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
}),
editing_message: None,
last_error: None,
_subscriptions: subscriptions,
};
@@ -127,44 +117,6 @@ impl ActiveThread {
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
let markdown = self.render_markdown(text.into(), window, cx);
self.rendered_messages_by_id.insert(*id, markdown);
self.list_state.scroll_to(ListOffset {
item_ix: old_len,
offset_in_item: Pixels(0.0),
});
}
fn edited_message(
&mut self,
id: &MessageId,
text: String,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
return;
};
self.list_state.splice(index..index + 1, 1);
let markdown = self.render_markdown(text.into(), window, cx);
self.rendered_messages_by_id.insert(*id, markdown);
}
fn deleted_message(&mut self, id: &MessageId) {
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
return;
};
self.messages.remove(index);
self.list_state.splice(index..index + 1, 0);
self.rendered_messages_by_id.remove(id);
}
fn render_markdown(
&self,
text: SharedString,
window: &Window,
cx: &mut Context<Self>,
) -> Entity<Markdown> {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
@@ -182,8 +134,6 @@ impl ActiveThread {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(0.).into())),
@@ -230,15 +180,20 @@ impl ActiveThread {
..Default::default()
};
cx.new(|cx| {
let markdown = cx.new(|cx| {
Markdown::new(
text,
text.into(),
markdown_style,
Some(self.language_registry.clone()),
None,
cx,
)
})
});
self.rendered_messages_by_id.insert(*id, markdown);
self.list_state.scroll_to(ListOffset {
item_ix: old_len,
offset_in_item: Pixels(0.0),
});
}
fn handle_thread_event(
@@ -253,7 +208,11 @@ impl ActiveThread {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
self.save_thread(cx);
self.thread_store
.update(cx, |thread_store, cx| {
thread_store.save_thread(&self.thread, cx)
})
.detach_and_log_err(cx);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
@@ -272,25 +231,12 @@ impl ActiveThread {
self.push_message(message_id, message_text, window, cx);
}
self.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageEdited(message_id) => {
if let Some(message_text) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.text.clone())
{
self.edited_message(message_id, message_text, window, cx);
}
self.thread_store
.update(cx, |thread_store, cx| {
thread_store.save_thread(&self.thread, cx)
})
.detach_and_log_err(cx);
self.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageDeleted(message_id) => {
self.deleted_message(message_id);
self.save_thread(cx);
cx.notify();
}
ThreadEvent::UsePendingTools => {
@@ -341,133 +287,7 @@ impl ActiveThread {
}
}
/// Spawns a task to save the active thread.
///
/// Only one task to save the thread will be in flight at a time.
fn save_thread(&mut self, cx: &mut Context<Self>) {
let thread = self.thread.clone();
self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
let task = this
.update(&mut cx, |this, cx| {
this.thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
})
.ok();
if let Some(task) = task {
task.await.log_err();
}
}));
}
fn start_editing_message(
&mut self,
message_id: MessageId,
message_text: String,
window: &mut Window,
cx: &mut Context<Self>,
) {
let buffer = cx.new(|cx| {
MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
});
let editor = cx.new(|cx| {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight { max_lines: 8 },
buffer,
None,
false,
window,
cx,
);
editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor
});
self.editing_message = Some((
message_id,
EditMessageState {
editor: editor.clone(),
},
));
cx.notify();
}
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.editing_message.take();
cx.notify();
}
fn confirm_editing_message(
&mut self,
_: &menu::Confirm,
_: &mut Window,
cx: &mut Context<Self>,
) {
let Some((message_id, state)) = self.editing_message.take() else {
return;
};
let edited_text = state.editor.read(cx).text(cx);
self.thread.update(cx, |thread, cx| {
thread.edit_message(message_id, Role::User, edited_text, cx);
for message_id in self.messages_after(message_id) {
thread.delete_message(*message_id, cx);
}
});
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
return;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.active_model() else {
return;
};
self.thread.update(cx, |thread, cx| {
thread.send_to_model(model, RequestKind::Chat, false, cx)
});
cx.notify();
}
fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
self.messages
.iter()
.rev()
.find(|message_id| {
self.thread
.read(cx)
.message(**message_id)
.map_or(false, |message| message.role == Role::User)
})
.cloned()
}
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
self.messages
.iter()
.position(|id| *id == message_id)
.map(|index| &self.messages[index + 1..])
.unwrap_or(&[])
}
fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_editing_message(&menu::Cancel, window, cx);
}
fn handle_regenerate_click(
&mut self,
_: &ClickEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.confirm_editing_message(&menu::Confirm, window, cx);
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
fn render_message(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -486,28 +306,8 @@ impl ActiveThread {
return Empty.into_any();
}
let allow_editing_message =
message.role == Role::User && self.last_user_message(cx) == Some(message_id);
let edit_message_editor = self
.editing_message
.as_ref()
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state.editor.clone());
let message_content = v_flex()
.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
div()
.key_context("EditMessageEditor")
.on_action(cx.listener(Self::cancel_editing_message))
.on_action(cx.listener(Self::confirm_editing_message))
.p_2p5()
.child(edit_message_editor)
} else {
div().p_2p5().text_ui(cx).child(markdown.clone())
},
)
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(
@@ -537,8 +337,7 @@ impl ActiveThread {
.child(
h_flex()
.py_1()
.pl_2()
.pr_1()
.px_2()
.bg(colors.editor_foreground.opacity(0.05))
.border_b_1()
.border_color(colors.border)
@@ -557,71 +356,6 @@ impl ActiveThread {
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when_some(
edit_message_editor.clone(),
|this, edit_message_editor| {
let focus_handle = edit_message_editor.focus_handle(cx);
this.child(
h_flex()
.gap_1()
.child(
Button::new("cancel-edit-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(
cx.listener(Self::handle_cancel_click),
),
)
.child(
Button::new(
"confirm-edit-message",
"Regenerate",
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(
cx.listener(Self::handle_regenerate_click),
),
),
)
},
)
.when(
edit_message_editor.is_none() && allow_editing_message,
|this| {
this.child(
Button::new("edit-message", "Edit")
.label_size(LabelSize::Small)
.on_click(cx.listener({
let message_text = message.text.clone();
move |this, _, window, cx| {
this.start_editing_message(
message_id,
message_text.clone(),
window,
cx,
);
}
})),
)
},
),
)
.child(message_content),
@@ -645,7 +379,7 @@ impl ActiveThread {
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.rounded_md()
.child(message_content),
),
};
@@ -674,7 +408,7 @@ impl ActiveThread {
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
.when(!is_open, |element| element.rounded_md())
.when(!is_open, |element| element.rounded(px(6.)))
.border_color(cx.theme().colors().border)
.child(
h_flex()

View File

@@ -134,7 +134,7 @@ impl AssistantConfiguration {
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_sm()
.rounded_md()
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(

View File

@@ -15,8 +15,7 @@ use editor::Editor;
use fs::Fs;
use gpui::{
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
WeakEntity,
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -994,21 +993,12 @@ impl AssistantPanel {
)
.into_any()
}
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AssistantPanel2");
if matches!(self.active_view, ActiveView::PromptEditor) {
key_context.add("prompt_editor");
}
key_context
}
}
impl Render for AssistantPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context(self.key_context())
.key_context("AssistantPanel2")
.justify_between()
.size_full()
.on_action(cx.listener(Self::cancel))
@@ -1023,7 +1013,12 @@ impl Render for AssistantPanel {
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.child(h_flex().child(self.message_editor.clone()))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),

View File

@@ -167,8 +167,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
}
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
"Enter the URL that you would like to fetch".into()
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Enter the URL that you would like to fetch".into())
}
fn selected_index(&self) -> usize {

View File

@@ -208,18 +208,12 @@ impl ContextStore {
let mut text_tasks = Vec::new();
this.update(&mut cx, |_, cx| {
for (path, buffer_entity) in files.into_iter().zip(buffers) {
// Skip all binary files and other non-UTF8 files
if let Ok(buffer_entity) = buffer_entity {
let buffer = buffer_entity.read(cx);
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
buffer_entity,
buffer,
cx.to_async(),
);
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
let buffer_entity = buffer_entity?;
let buffer = buffer_entity.read(cx);
let (buffer_info, text_task) =
collect_buffer_info_and_text(path, buffer_entity, buffer, cx.to_async());
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
anyhow::Ok(())
})??;

View File

@@ -27,7 +27,6 @@ use language::{Buffer, Point, Selection, TransactionId};
use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::ActionVariant;
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore};
@@ -1728,10 +1727,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
lsp_action: lsp::CodeAction {
title: "Fix with Assistant".into(),
..Default::default()
})),
},
}]))
} else {
Task::ready(Ok(Vec::new()))

View File

@@ -4,8 +4,8 @@ use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity,
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
TextStyle, WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
@@ -16,7 +16,7 @@ use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
Tooltip,
TintColor, Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
@@ -298,210 +298,166 @@ impl Render for MessageEditor {
let linux = platform == PlatformStyle::Linux;
let windows = platform == PlatformStyle::Windows;
let button_width = if linux || windows || vim_mode_enabled {
px(82.)
px(92.)
} else {
px(64.)
};
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::toggle_chat_mode))
.size_full()
.when(is_streaming_completion, |parent| {
let focus_handle = self.editor.focus_handle(cx).clone();
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.pl_2()
.pr_1()
.py_1()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.shadow_md()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(gpui::Transformation::rotate(
gpui::percentage(delta),
))
},
),
)
.child(
Label::new("Generating…")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(ui::Divider::vertical())
.child(
Button::new("cancel-generation", "Cancel")
.label_size(LabelSize::XSmall)
.key_binding(
KeyBinding::for_action_in(
&editor::actions::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
}),
),
),
)
})
.gap_2()
.p_2()
.bg(bg_color)
.child(self.context_strip.clone())
.child(
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::toggle_chat_mode))
.gap_2()
.p_2()
.bg(bg_color)
.border_t_1()
.border_color(cx.theme().colors().border)
.child(self.context_strip.clone())
.gap_5()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
v_flex()
.gap_5()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
PopoverMenu::new("inline-context-picker")
.menu(move |window, cx| {
inline_context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
Some(inline_context_picker.clone())
})
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.justify_between()
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |window, cx| {
inline_context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(inline_context_picker.clone())
})
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
- px(4.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
}))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
)
.child(
h_flex()
.justify_between()
.child(
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(
|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
},
))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
)
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message")
.width(button_width.into())
.style(ButtonStyle::Filled)
.disabled(
is_editor_empty
|| !is_model_selected
|| is_streaming_completion,
)
.child(h_flex().gap_1().child(self.model_selector.clone()).child(
if is_streaming_completion {
ButtonLike::new("cancel-generation")
.width(button_width.into())
.style(ButtonStyle::Tinted(TintColor::Accent))
.child(
h_flex()
.w_full()
.justify_between()
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Submit")
.size(LabelSize::Small)
.color(submit_label_color),
)
.children(
KeyBinding::for_action_in(
&Chat,
&focus_handle,
window,
cx,
)
.map(|binding| {
binding
.when(vim_mode_enabled, |kb| {
kb.size(rems_from_px(12.))
})
.into_any_element()
}),
Label::new("Cancel")
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(
0.4, 0.8,
)),
|label, delta| label.alpha(delta),
),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text(
"Type a message to submit",
))
})
.when(is_streaming_completion, |button| {
button.tooltip(Tooltip::text(
"Cancel to submit a new message",
))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
}),
),
),
),
.children(
KeyBinding::for_action_in(
&editor::actions::Cancel,
&focus_handle,
window,
cx,
)
.map(|binding| binding.into_any_element()),
),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
})
} else {
ButtonLike::new("submit-message")
.width(button_width.into())
.style(ButtonStyle::Filled)
.disabled(is_editor_empty || !is_model_selected)
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Submit")
.size(LabelSize::Small)
.color(submit_label_color),
)
.children(
KeyBinding::for_action_in(
&Chat,
&focus_handle,
window,
cx,
)
.map(|binding| binding.into_any_element()),
),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
})
.when(is_editor_empty, |button| {
button
.tooltip(Tooltip::text("Type a message to submit"))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
})
},
)),
),
)
}

View File

@@ -8,9 +8,8 @@ use futures::StreamExt as _;
use gpui::{App, Context, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolUseId,
MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, Role, StopReason,
};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
@@ -89,7 +88,7 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use: ToolUseState::new(),
tool_use: ToolUseState::default(),
}
}
@@ -99,14 +98,7 @@ impl Thread {
tools: Arc<ToolWorkingSet>,
_cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
saved
.messages
.last()
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
let next_message_id = MessageId(saved.messages.len());
Self {
id,
@@ -128,7 +120,7 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use,
tool_use: ToolUseState::default(),
}
}
@@ -197,10 +189,6 @@ impl Thread {
self.tool_use.tool_uses_for_message(id)
}
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
self.tool_use.tool_results_for_message(id)
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
@@ -235,34 +223,6 @@ impl Thread {
id
}
pub fn edit_message(
&mut self,
id: MessageId,
new_role: Role,
new_text: String,
cx: &mut Context<Self>,
) -> bool {
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
return false;
};
message.role = new_role;
message.text = new_text;
self.touch_updated_at();
cx.emit(ThreadEvent::MessageEdited(id));
true
}
pub fn delete_message(&mut self, id: MessageId, cx: &mut Context<Self>) -> bool {
let Some(index) = self.messages.iter().position(|message| message.id == id) else {
return false;
};
self.messages.remove(index);
self.context_by_message.remove(&id);
self.touch_updated_at();
cx.emit(ThreadEvent::MessageDeleted(id));
true
}
/// Returns the representation of this [`Thread`] in a textual form.
///
/// This is the representation we use when attaching a thread as context to another thread.
@@ -601,8 +561,6 @@ pub enum ThreadEvent {
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
MessageEdited(MessageId),
MessageDeleted(MessageId),
SummaryChanged,
UsePendingTools,
ToolFinished {

View File

@@ -12,9 +12,9 @@ use futures::FutureExt as _;
use gpui::{
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
};
use heed::types::{SerdeBincode, SerdeJson};
use heed::types::SerdeBincode;
use heed::Database;
use language_model::{LanguageModelToolUseId, Role};
use language_model::Role;
use project::Project;
use serde::{Deserialize, Serialize};
use util::ResultExt as _;
@@ -113,24 +113,6 @@ impl ThreadStore {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: thread
.tool_uses_for_message(message.id)
.into_iter()
.map(|tool_use| SavedToolUse {
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
})
.collect(),
tool_results: thread
.tool_results_for_message(message.id)
.into_iter()
.map(|tool_result| SavedToolResult {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
})
.collect(),
})
.collect(),
};
@@ -257,29 +239,11 @@ pub struct SavedThread {
pub messages: Vec<SavedMessage>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub tool_uses: Vec<SavedToolUse>,
#[serde(default)]
pub tool_results: Vec<SavedToolResult>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SavedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SavedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: Arc<str>,
}
struct GlobalThreadsDatabase(
@@ -291,7 +255,7 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
threads: Database<SerdeBincode<ThreadId>, SerdeBincode<SavedThread>>,
}
impl ThreadsDatabase {
@@ -306,7 +270,7 @@ impl ThreadsDatabase {
let database_future = executor
.spawn({
let executor = executor.clone();
let database_path = paths::support_dir().join("threads/threads-db.1.mdb");
let database_path = paths::support_dir().join("threads/threads-db.0.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))

View File

@@ -7,11 +7,10 @@ use futures::FutureExt as _;
use gpui::{SharedString, Task};
use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role,
LanguageModelToolUseId, MessageContent,
};
use crate::thread::MessageId;
use crate::thread_store::SavedMessage;
#[derive(Debug)]
pub struct ToolUse {
@@ -29,6 +28,7 @@ pub enum ToolUseStatus {
Error(SharedString),
}
#[derive(Default)]
pub struct ToolUseState {
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
@@ -37,65 +37,6 @@ pub struct ToolUseState {
}
impl ToolUseState {
pub fn new() -> Self {
Self {
tool_uses_by_assistant_message: HashMap::default(),
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
}
}
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
let mut this = Self::new();
for message in messages {
match message.role {
Role::Assistant => {
if !message.tool_uses.is_empty() {
this.tool_uses_by_assistant_message.insert(
message.id,
message
.tool_uses
.iter()
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
})
.collect(),
);
}
}
Role::User => {
if !message.tool_results.is_empty() {
let tool_uses_by_user_message = this
.tool_uses_by_user_message
.entry(message.id)
.or_default();
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
tool_uses_by_user_message.push(tool_use_id.clone());
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id,
is_error: tool_result.is_error,
content: tool_result.content.clone(),
},
);
}
}
}
Role::System => {}
}
}
this
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
@@ -143,17 +84,6 @@ impl ToolUseState {
tool_uses
}
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
let empty = Vec::new();
self.tool_uses_by_user_message
.get(&message_id)
.unwrap_or(&empty)
.iter()
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
.collect()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_uses_by_user_message
.get(&message_id)

View File

@@ -103,7 +103,7 @@ impl RenderOnce for ContextPill {
.pl_1()
.pb(px(1.))
.border_1()
.rounded_sm()
.rounded_md()
.gap_1()
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));

View File

@@ -54,7 +54,7 @@ use ui::{
Tooltip,
};
use util::{maybe, ResultExt};
use workspace::searchable::{Direction, SearchableItemHandle};
use workspace::searchable::SearchableItemHandle;
use workspace::{
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
@@ -1241,7 +1241,7 @@ impl ContextEditor {
.child("Press")
.child(
h_flex()
.rounded_sm()
.rounded_md()
.px_1()
.mr_0p5()
.border_1()
@@ -2092,7 +2092,7 @@ impl ContextEditor {
.ml(gutter_width)
.pb_1()
.w(max_width - gutter_width)
.rounded_sm()
.rounded_md()
.border_1()
.border_color(theme.colors().border_variant)
.overflow_hidden()
@@ -3106,13 +3106,12 @@ impl SearchableItem for ContextEditor {
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Self::Match],
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
self.editor.update(cx, |editor, cx| {
editor.active_match_index(direction, matches, window, cx)
editor.active_match_index(matches, window, cx)
})
}
}
@@ -3422,7 +3421,7 @@ fn invoked_slash_command_fold_placeholder(
.ml_6()
.gap_2()
.bg(cx.theme().colors().surface_background)
.rounded_sm()
.rounded_md()
.child(Label::new(format!("/{}", command.name.clone())))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {

View File

@@ -104,49 +104,53 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
context_server_manager,
context_server_slash_command_ids: HashMap::default(),
host_contexts: Vec::new(),
fs,
languages,
slash_commands,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
let this =
cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
cx,
)
});
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
context_server_manager,
context_server_slash_command_ids: HashMap::default(),
host_contexts: Vec::new(),
fs,
languages,
slash_commands,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
anyhow::Ok(())
}
anyhow::Ok(())
}
.log_err()
}),
client_subscription: None,
_project_subscriptions: vec![
cx.observe(&project, Self::handle_project_changed),
cx.subscribe(&project, Self::handle_project_event),
],
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
this
})?;
.log_err()
}),
client_subscription: None,
_project_subscriptions: vec![
cx.subscribe(&project, Self::handle_project_event)
],
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_shared(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
this
})?;
Ok(this)
})
@@ -288,7 +292,7 @@ impl ContextStore {
})?
}
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
@@ -318,11 +322,14 @@ impl ContextStore {
fn handle_project_event(
&mut self,
_: Entity<Project>,
project: Entity<Project>,
event: &project::Event,
cx: &mut Context<Self>,
) {
match event {
project::Event::RemoteIdChanged(_) => {
self.handle_project_shared(project, cx);
}
project::Event::Reshared => {
self.advertise_contexts(cx);
}

View File

@@ -16,7 +16,6 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
gpui.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,19 +1,13 @@
mod list_worktrees_tool;
mod now_tool;
mod read_file_tool;
use assistant_tool::ToolRegistry;
use gpui::App;
use crate::list_worktrees_tool::ListWorktreesTool;
use crate::now_tool::NowTool;
use crate::read_file_tool::ReadFileTool;
pub fn init(cx: &mut App) {
assistant_tool::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(NowTool);
registry.register_tool(ListWorktreesTool);
registry.register_tool(ReadFileTool);
}

View File

@@ -1,84 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Task, WeakEntity, Window};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListWorktreesToolInput {}
pub struct ListWorktreesTool;
impl Tool for ListWorktreesTool {
fn name(&self) -> String {
"list-worktrees".into()
}
fn description(&self) -> String {
"Lists all worktrees in the current project. Use this tool when you need to find available worktrees and their IDs.".into()
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!(
{
"type": "object",
"properties": {},
"required": []
}
)
}
fn run(
self: Arc<Self>,
_input: serde_json::Value,
workspace: WeakEntity<Workspace>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<String>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace dropped")));
};
let project = workspace.read(cx).project().clone();
cx.spawn(|cx| async move {
cx.update(|cx| {
#[derive(Debug, Serialize)]
struct WorktreeInfo {
id: usize,
root_name: String,
root_dir: Option<String>,
}
let worktrees = project.update(cx, |project, cx| {
project
.visible_worktrees(cx)
.map(|worktree| {
worktree.read_with(cx, |worktree, _cx| WorktreeInfo {
id: worktree.id().to_usize(),
root_dir: worktree
.root_dir()
.map(|root_dir| root_dir.to_string_lossy().to_string()),
root_name: worktree.root_name().to_string(),
})
})
.collect::<Vec<_>>()
});
if worktrees.is_empty() {
return Ok("No worktrees found in the current project.".to_string());
}
let mut result = String::from("Worktrees in the current project:\n\n");
for worktree in worktrees {
result.push_str(&serde_json::to_string(&worktree)?);
}
Ok(result)
})?
})
}
}

View File

@@ -1,69 +0,0 @@
use std::path::Path;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Task, WeakEntity, Window};
use project::{ProjectPath, WorktreeId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput {
/// The ID of the worktree in which the file resides.
pub worktree_id: usize,
/// The path to the file to read.
///
/// This path is relative to the worktree root, it must not be an absolute path.
pub path: Arc<Path>,
}
pub struct ReadFileTool;
impl Tool for ReadFileTool {
fn name(&self) -> String {
"read-file".into()
}
fn description(&self) -> String {
"Reads the content of a file specified by a worktree ID and path. Use this tool when you need to access the contents of a file in the project.".into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(ReadFileToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
workspace: WeakEntity<Workspace>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<String>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace dropped")));
};
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let project = workspace.read(cx).project().clone();
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(input.worktree_id),
path: input.path,
};
cx.spawn(|cx| async move {
let buffer = cx
.update(|cx| {
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
})?
.await?;
cx.update(|cx| buffer.read(cx).text())
})
}
}

View File

@@ -838,8 +838,8 @@ impl BufferDiff {
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> anyhow::Result<Option<Range<Anchor>>> {
let snapshot = if base_text_changed || language_changed {
) -> anyhow::Result<BufferDiffSnapshot> {
let inner = if base_text_changed || language_changed {
cx.update(|cx| {
Self::build(
buffer.clone(),
@@ -861,18 +861,45 @@ impl BufferDiff {
})?
.await
};
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
Ok(BufferDiffSnapshot {
inner,
secondary_diff: None,
})
}
pub fn update_diff_from(
pub fn set_snapshot(
&mut self,
buffer: &text::BufferSnapshot,
other: &Entity<Self>,
new_snapshot: BufferDiffSnapshot,
language_changed: bool,
secondary_changed_range: Option<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
let other = other.read(cx).inner.clone();
self.set_state(other, buffer)
let changed_range = self.set_state(new_snapshot.inner, buffer);
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
let changed_range = match (secondary_changed_range, changed_range) {
(None, None) => None,
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
(None, Some(uncommitted_range)) => Some(uncommitted_range),
(Some(unstaged_range), Some(uncommitted_range)) => {
let mut start = uncommitted_range.start;
let mut end = uncommitted_range.end;
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
{
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
}
Some(start..end)
}
};
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: changed_range.clone(),
});
changed_range
}
fn set_state(

View File

@@ -396,6 +396,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)

View File

@@ -3,7 +3,6 @@ use crate::{
tests::{rust_lang, TestServer},
};
use call::ActiveCall;
use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
@@ -1983,7 +1982,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
blame_entry("3a3a3a", 2..3),
blame_entry("4c4c4c", 3..4),
],
permalinks: HashMap::default(), // This field is deprecrated
messages: [
("1b1b1b", "message for idx-0"),
("0d0d0d", "message for idx-1"),
@@ -2040,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

@@ -271,7 +271,7 @@ impl TestServer {
let git_hosting_provider_registry = cx.update(GitHostingProviderRegistry::default_global);
git_hosting_provider_registry
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
.register_hosting_provider(Arc::new(git_hosting_providers::Github::new()));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));

View File

@@ -323,7 +323,7 @@ impl ChatPanel {
.my_0p5()
.px_0p5()
.gap_x_1()
.rounded_sm()
.rounded_md()
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
.when(reply_to_message.is_none(), |el| {
el.child(
@@ -358,7 +358,7 @@ impl ChatPanel {
.my_0p5()
.px_0p5()
.gap_x_1()
.rounded_sm()
.rounded_md()
.overflow_hidden()
.hover(|style| style.bg(cx.theme().colors().element_background))
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
@@ -476,7 +476,7 @@ impl ChatPanel {
div()
.group("")
.bg(background)
.rounded_sm()
.rounded_md()
.overflow_hidden()
.px_1p5()
.py_0p5()
@@ -563,7 +563,7 @@ impl ChatPanel {
.child(
div()
.px_1()
.rounded_sm()
.rounded_md()
.text_ui_xs(cx)
.bg(cx.theme().colors().background)
.child("New messages"),
@@ -589,7 +589,7 @@ impl ChatPanel {
div()
.w_6()
.bg(cx.theme().colors().element_background)
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_sm())
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_md())
.child(child)
}
@@ -604,7 +604,7 @@ impl ChatPanel {
.absolute()
.right_2()
.overflow_hidden()
.rounded_sm()
.rounded_md()
.border_color(cx.theme().colors().element_selected)
.border_1()
.when(!self.has_open_menu(message_id), |el| {

View File

@@ -531,7 +531,7 @@ impl Render for MessageEditor {
.px_2()
.py_1()
.bg(cx.theme().colors().editor_background)
.rounded_sm()
.rounded_md()
.child(EditorElement::new(
&self.editor,
EditorStyle {

View File

@@ -300,7 +300,7 @@ impl NotificationPanel {
.hover(|style| {
style
.bg(cx.theme().colors().element_selected)
.rounded_sm()
.rounded_md()
})
.child(Label::new(relative_timestamp).color(Color::Muted))
.tooltip(move |_, cx| {

View File

@@ -306,7 +306,7 @@ impl ComponentPreview {
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.rounded_md()
.w_full()
.gap_4()
.py_4()
@@ -341,22 +341,17 @@ impl ComponentPreview {
.into_any_element()
}
fn test_status_toast(&self, window: &mut Window, cx: &mut Context<Self>) {
fn test_status_toast(&self, cx: &mut Context<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
let status_toast = StatusToast::new(
"`zed/new-notification-system` created!",
window,
cx,
|this, _, cx| {
let status_toast =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action(
"Open Pull Request",
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
)
},
);
workspace.toggle_status_toast(window, cx, status_toast)
.action("Open Pull Request", |_, cx| {
cx.open_url("https://github.com/")
})
});
workspace.toggle_status_toast(status_toast, cx)
});
}
}
@@ -406,8 +401,8 @@ impl Render for ComponentPreview {
div().w_full().pb_4().child(
Button::new("toast-test", "Launch Toast")
.on_click(cx.listener({
move |this, _, window, cx| {
this.test_status_toast(window, cx);
move |this, _, _window, cx| {
this.test_status_toast(cx);
cx.notify();
}
}))

View File

@@ -122,7 +122,7 @@ impl CopilotCodeVerification {
.p_1()
.border_1()
.border_muted(cx)
.rounded_sm()
.rounded_md()
.cursor_pointer()
.justify_between()
.on_mouse_down(gpui::MouseButton::Left, {

View File

@@ -973,7 +973,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
h_flex()
.gap_2()
.px_1()
.rounded_sm()
.rounded_md()
.bg(color.surface_background.opacity(0.5))
.map(|stack| {
stack.child(

View File

@@ -94,7 +94,6 @@ ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages = {workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
multi_buffer = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }

View File

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

View File

@@ -7,6 +7,7 @@ use std::time::Duration;
pub struct BlinkManager {
blink_interval: Duration,
blink_epoch: usize,
blinking_paused: bool,
visible: bool,
@@ -23,6 +24,7 @@ impl BlinkManager {
Self {
blink_interval,
blink_epoch: 0,
blinking_paused: false,
visible: true,

View File

@@ -8,7 +8,7 @@ use crate::lsp_ext::find_specific_language_server_in_selection;
use crate::{element::register_action, Editor, SwitchSourceHeader};
use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
const CLANGD_SERVER_NAME: &str = "clangd";
fn is_c_language(language: &Language) -> bool {
return language.name() == "C++".into() || language.name() == "C".into();
@@ -47,7 +47,7 @@ pub fn switch_source_header(
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_store::lsp_ext_command::SwitchSourceHeader,
project::lsp_ext_command::SwitchSourceHeader,
cx,
)
});

View File

@@ -534,7 +534,7 @@ impl CompletionsMenu {
};
let color_swatch = completion
.color()
.map(|color| div().size_4().bg(color).rounded_xs());
.map(|color| div().size_4().bg(color).rounded_sm());
div().min_w(px(280.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
@@ -851,7 +851,7 @@ impl CodeActionsItem {
pub fn label(&self) -> String {
match self {
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
Self::Task(_, task) => task.resolved_label.clone(),
}
}
@@ -984,7 +984,7 @@ impl CodeActionsMenu {
.overflow_hidden()
.child(
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
action.lsp_action.title().replace("\n", ""),
action.lsp_action.title.replace("\n", ""),
)
.when(selected, |this| {
this.text_color(colors.text_accent)
@@ -1029,7 +1029,7 @@ impl CodeActionsMenu {
.max_by_key(|(_, action)| match action {
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title().chars().count()
action.lsp_action.title.chars().count()
}
})
.map(|(ix, _)| ix),

View File

@@ -43,7 +43,7 @@ use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, Under
pub use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::{is_invisible, replacement};
use invisibles::{is_invisible, replacement};
use language::{
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,

View File

@@ -45,7 +45,7 @@ pub fn is_invisible(c: char) -> bool {
// ASCII control characters have fancy unicode glyphs, everything else
// is replaced by a space - unless it is used in combining characters in
// which case we need to leave it in the string.
pub fn replacement(c: char) -> Option<&'static str> {
pub(crate) fn replacement(c: char) -> Option<&'static str> {
if c <= '\x1f' {
Some(C0_SYMBOLS[c as usize])
} else if c == '\x7f' {

View File

@@ -28,7 +28,6 @@ mod hover_popover;
mod indent_guides;
mod inlay_hint_cache;
pub mod items;
mod jsx_tag_auto_close;
mod linked_editing_ranges;
mod lsp_ext;
mod mouse_context_menu;
@@ -86,8 +85,8 @@ use gpui::{
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
WeakEntity, WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
@@ -606,12 +605,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.
@@ -643,6 +636,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>>,
@@ -726,7 +720,6 @@ pub struct Editor {
use_autoclose: bool,
use_auto_surround: bool,
auto_replace_emoji_shortcode: bool,
jsx_tag_auto_close_enabled_in_any_buffer: bool,
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
@@ -1202,7 +1195,7 @@ impl Editor {
.bg(cx.theme().colors().ghost_element_background)
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.rounded_xs()
.rounded_sm()
.size_full()
.cursor_pointer()
.child("")
@@ -1250,11 +1243,6 @@ impl Editor {
let mut project_subscriptions = Vec::new();
if mode == EditorMode::Full {
if let Some(project) = project.as_ref() {
if buffer.read(cx).is_singleton() {
project_subscriptions.push(cx.observe_in(project, window, |_, _, _, cx| {
cx.emit(EditorEvent::TitleChanged);
}));
}
project_subscriptions.push(cx.subscribe_in(
project,
window,
@@ -1359,6 +1347,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 _),
@@ -1413,7 +1402,6 @@ impl Editor {
use_autoclose: true,
use_auto_surround: true,
auto_replace_emoji_shortcode: false,
jsx_tag_auto_close_enabled_in_any_buffer: false,
leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
@@ -1497,7 +1485,6 @@ impl Editor {
this.end_selection(window, cx);
this.scroll_manager.show_scrollbar(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
if mode == EditorMode::Full {
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
@@ -3106,9 +3093,6 @@ impl Editor {
drop(snapshot);
self.transact(window, cx, |this, window, cx| {
let initial_buffer_versions =
jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, this.autoindent_mode.clone(), cx);
});
@@ -3193,10 +3177,22 @@ 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);
jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
});
}
@@ -6346,9 +6342,6 @@ impl Editor {
const BORDER_WIDTH: Pixels = px(1.);
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
let mut element = h_flex()
.items_start()
.child(
@@ -6379,19 +6372,7 @@ impl Editor {
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.id("edit_prediction_diff_popover_keybind")
.when(!has_keybind, |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.child(Icon::new(IconName::Info).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
})
})
.children(keybind),
.children(self.render_edit_prediction_accept_keybind(window, cx)),
)
.into_any();
@@ -6489,11 +6470,7 @@ impl Editor {
}
}
fn render_edit_prediction_accept_keybind(
&self,
window: &mut Window,
cx: &App,
) -> Option<AnyElement> {
fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
let accept_keystroke = accept_binding.keystroke()?;
@@ -6529,7 +6506,6 @@ impl Editor {
.size(Some(IconSize::XSmall.rems().into())),
)
})
.into_any()
.into()
}
@@ -6539,52 +6515,21 @@ impl Editor {
icon: Option<IconName>,
window: &mut Window,
cx: &App,
) -> Option<Stateful<Div>> {
) -> Option<Div> {
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
let result = h_flex()
.id("ep-line-popover")
.py_0p5()
.pl_1()
.pr(padding_right)
.gap_1()
.rounded_md()
.rounded(px(6.))
.border_1()
.bg(Self::edit_prediction_line_popover_bg_color(cx))
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
.shadow_sm()
.when(!has_keybind, |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.pl_2()
.child(Icon::new(IconName::ZedPredictError).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
})
})
.children(keybind)
.child(
Label::new(label)
.size(LabelSize::Small)
.when(!has_keybind, |el| {
el.color(cx.theme().status().error.into()).strikethrough()
}),
)
.when(!has_keybind, |el| {
el.child(
h_flex().ml_1().child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(cx.theme().status().error.into()),
),
)
})
.children(self.render_edit_prediction_accept_keybind(window, cx))
.child(Label::new(label).size(LabelSize::Small))
.when_some(icon, |element, icon| {
element.child(
div()
@@ -6681,9 +6626,6 @@ impl Editor {
.elevation_2(cx)
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.when(accept_keystroke.is_none(), |el| {
el.border_color(cx.theme().status().error)
})
.rounded(RADIUS)
.rounded_tl(px(0.))
.overflow_hidden()
@@ -6712,37 +6654,16 @@ impl Editor {
el.child(
Label::new("Hold")
.size(LabelSize::Small)
.when(accept_keystroke.is_none(), |el| {
el.strikethrough()
})
.line_height_style(LineHeightStyle::UiLabel),
)
})
.id("edit_prediction_cursor_popover_keybind")
.when(accept_keystroke.is_none(), |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.child(Icon::new(IconName::Info).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip)
.into()
})
})
.when_some(
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
false,
)))
},
),
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke?.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
false,
))),
)
.into_any(),
);
@@ -8515,10 +8436,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();
@@ -8592,7 +8513,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;
}
@@ -8640,9 +8563,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(" "),
@@ -8653,7 +8578,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 {
@@ -11548,7 +11473,7 @@ impl Editor {
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_or_before_position(
self.go_to_hunk_before_or_after_position(
&snapshot,
selection.head(),
Direction::Next,
@@ -11557,7 +11482,7 @@ impl Editor {
);
}
fn go_to_hunk_after_or_before_position(
fn go_to_hunk_before_or_after_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
@@ -11608,7 +11533,7 @@ impl Editor {
) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_or_before_position(
self.go_to_hunk_before_or_after_position(
&snapshot,
selection.head(),
Direction::Prev,
@@ -13770,23 +13695,38 @@ impl Editor {
return;
}
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let run_twice = snapshot
.hunks_for_ranges([newest_range])
.first()
.is_some_and(|hunk| {
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
self.hunk_after_position(&snapshot, next_line)
.is_some_and(|other| other.row_range == hunk.row_range)
});
if run_twice {
self.go_to_next_hunk(&GoToHunk, window, cx);
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
self.go_to_next_hunk(&GoToHunk, window, cx);
let snapshot = self.snapshot(window, cx);
let position = self.selections.newest::<Point>(cx).head();
let mut row = snapshot
.buffer_snapshot
.diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point())
.find(|hunk| hunk.row_range.start.0 > position.row)
.map(|hunk| hunk.row_range.start);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
// Outside of the project diff editor, wrap around to the beginning.
if !all_diff_hunks_expanded {
row = row.or_else(|| {
snapshot
.buffer_snapshot
.diff_hunks_in_range(Point::zero()..position)
.find(|hunk| hunk.row_range.end.0 < position.row)
.map(|hunk| hunk.row_range.start)
});
}
if let Some(row) = row {
let destination = Point::new(row.0, 0);
let autoscroll = Autoscroll::center();
self.unfold_ranges(&[destination..destination], false, false, cx);
self.change_selections(Some(autoscroll), window, cx, |s| {
s.select_ranges([destination..destination]);
});
} else if all_diff_hunks_expanded {
window.dispatch_action(::git::ExpandCommitEditor.boxed_clone(), cx);
}
}
fn do_stage_or_unstage(
@@ -14116,6 +14056,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);
}
@@ -14391,7 +14336,7 @@ impl Editor {
pub fn toggle_git_blame(
&mut self,
_: &ToggleGitBlame,
_: &::git::Blame,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -14887,14 +14832,14 @@ impl Editor {
&self,
window: &mut Window,
cx: &mut App,
) -> BTreeMap<DisplayRow, Background> {
) -> BTreeMap<DisplayRow, LineHighlight> {
let snapshot = self.snapshot(window, cx);
let mut used_highlight_orders = HashMap::default();
self.highlighted_rows
.iter()
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
.fold(
BTreeMap::<DisplayRow, Background>::new(),
BTreeMap::<DisplayRow, LineHighlight>::new(),
|mut unique_rows, highlight| {
let start = highlight.range.start.to_display_point(&snapshot);
let end = highlight.range.end.to_display_point(&snapshot);
@@ -15407,7 +15352,6 @@ impl Editor {
let buffer = self.buffer.read(cx);
self.registered_buffers
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::ExcerptsEdited {
@@ -15427,7 +15371,6 @@ impl Editor {
}
multi_buffer::Event::Reparsed(buffer_id) => {
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::Reparsed(*buffer_id));
}
@@ -15436,20 +15379,14 @@ impl Editor {
}
multi_buffer::Event::LanguageChanged(buffer_id) => {
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::Reparsed(*buffer_id));
cx.notify();
}
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
cx.emit(EditorEvent::TitleChanged)
}
// multi_buffer::Event::DiffBaseChanged => {
// self.scrollbar_marker_state.dirty = true;
// cx.emit(EditorEvent::DiffBaseChanged);
// cx.notify();
// }
multi_buffer::Event::FileHandleChanged
| multi_buffer::Event::Reloaded
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
@@ -18403,36 +18340,26 @@ fn all_edits_insertions_or_deletions(
all_insertions || all_deletions
}
struct MissingEditPredictionKeybindingTooltip;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineHighlight {
pub background: Background,
pub border: Option<gpui::Hsla>,
}
impl Render for MissingEditPredictionKeybindingTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
ui::tooltip_container(window, cx, |container, _, cx| {
container
.flex_shrink_0()
.max_w_80()
.min_h(rems_from_px(124.))
.justify_between()
.child(
v_flex()
.flex_1()
.text_ui_sm(cx)
.child(Label::new("Conflict with Accept Keybinding"))
.child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.")
)
.child(
h_flex()
.pb_1()
.gap_1()
.items_end()
.w_full()
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
}))
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
})),
)
})
impl From<Hsla> for LineHighlight {
fn from(hsla: Hsla) -> Self {
Self {
background: hsla.into(),
border: None,
}
}
}
impl From<Background> for LineHighlight {
fn from(background: Background) -> Self {
Self {
background,
border: None,
}
}
}

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, |_| {});
@@ -16807,245 +16832,6 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
"});
}
mod autoclose_tags {
use super::*;
use language::language_settings::JsxTagAutoCloseSettings;
use languages::language;
async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
init_test(cx, |settings| {
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
buffer.set_language(Some(language), cx)
});
cx
}
macro_rules! check {
($name:ident, $initial:literal + $input:literal => $expected:expr) => {
#[gpui::test]
async fn $name(cx: &mut TestAppContext) {
let mut cx = test_setup(cx).await;
cx.set_state($initial);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input($input, window, cx);
});
cx.run_until_parked();
cx.assert_editor_state($expected);
}
};
}
check!(
test_basic,
"<divˇ" + ">" => "<div>ˇ</div>"
);
check!(
test_basic_nested,
"<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
);
check!(
test_basic_ignore_already_closed,
"<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
);
check!(
test_doesnt_autoclose_closing_tag,
"</divˇ" + ">" => "</div>ˇ"
);
check!(
test_jsx_attr,
"<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
);
check!(
test_ignores_closing_tags_in_expr_block,
"<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
);
check!(
test_doesnt_autoclose_on_gt_in_expr,
"<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
);
check!(
test_ignores_closing_tags_with_different_tag_names,
"<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
);
check!(
test_autocloses_in_jsx_expression,
"<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
);
check!(
test_doesnt_autoclose_already_closed_in_jsx_expression,
"<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
);
check!(
test_autocloses_fragment,
"" + ">" => "<>ˇ</>"
);
check!(
test_does_not_include_type_argument_in_autoclose_tag_name,
"<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
);
check!(
test_does_not_autoclose_doctype,
"<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
);
check!(
test_does_not_autoclose_comment,
"<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
);
check!(
test_multi_cursor_autoclose_same_tag,
r#"
<divˇ
<divˇ
"#
+ ">" =>
r#"
<div>ˇ</div>
<div>ˇ</div>
"#
);
check!(
test_multi_cursor_autoclose_different_tags,
r#"
<divˇ
<spanˇ
"#
+ ">" =>
r#"
<div>ˇ</div>
<span>ˇ</span>
"#
);
check!(
test_multi_cursor_autoclose_some_dont_autoclose_others,
r#"
<divˇ
<div /ˇ
<spanˇ</span>
<!DOCTYPE htmlˇ
</headˇ
<Component<T>ˇ
ˇ
"#
+ ">" =>
r#"
<div>ˇ</div>
<div />ˇ
<span>ˇ</span>
<!DOCTYPE html>ˇ
</head>ˇ
<Component<T>>ˇ</Component>
"#
);
check!(
test_doesnt_mess_up_trailing_text,
"<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
);
#[gpui::test]
async fn test_multibuffer(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
});
let buffer_a = cx.new(|cx| {
let mut buf = language::Buffer::local("<div", cx);
buf.set_language(
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
cx,
);
buf
});
let buffer_b = cx.new(|cx| {
let mut buf = language::Buffer::local("<pre", cx);
buf.set_language(
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
cx,
);
buf
});
let buffer_c = cx.new(|cx| {
let buf = language::Buffer::local("<span", cx);
buf
});
let buffer = cx.new(|cx| {
let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
buf.push_excerpts(
buffer_a,
[ExcerptRange {
context: text::Anchor::MIN..text::Anchor::MAX,
primary: None,
}],
cx,
);
buf.push_excerpts(
buffer_b,
[ExcerptRange {
context: text::Anchor::MIN..text::Anchor::MAX,
primary: None,
}],
cx,
);
buf.push_excerpts(
buffer_c,
[ExcerptRange {
context: text::Anchor::MIN..text::Anchor::MAX,
primary: None,
}],
cx,
);
buf
});
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
let mut cx = EditorTestContext::for_editor(editor, cx).await;
cx.update_editor(|editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
selections.select(vec![
Selection::from_offset(4),
Selection::from_offset(9),
Selection::from_offset(15),
])
})
});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input(">", window, cx);
});
cx.run_until_parked();
cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
}
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -20,10 +20,10 @@ use crate::{
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
@@ -32,15 +32,14 @@ use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
TextStyleRefinement, Window,
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, solid_background, svg, transparent_black, Action, AnyElement, App,
AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window,
};
use inline_completion::Direction;
use itertools::Itertools;
@@ -56,7 +55,7 @@ use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
};
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use settings::Settings;
use smallvec::{smallvec, SmallVec};
use std::{
@@ -1691,7 +1690,7 @@ impl EditorElement {
let pos_y = content_origin.y
+ line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
let window_ix = row.minus(start_row) as usize;
let window_ix = row.0.saturating_sub(start_row.0) as usize;
let pos_x = {
let crease_trailer_layout = &crease_trailers[window_ix];
let line_layout = &line_layouts[window_ix];
@@ -1724,7 +1723,7 @@ impl EditorElement {
.h(line_height)
.w_full()
.px_1()
.rounded_xs()
.rounded_sm()
.opacity(opacity)
.bg(severity_to_color(&diagnostic_to_render.severity)
.color(cx)
@@ -2717,7 +2716,7 @@ impl EditorElement {
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.pl_0p5()
.pr_5()
.rounded_sm()
.rounded_md()
.shadow_md()
.border_1()
.map(|div| {
@@ -2741,7 +2740,7 @@ impl EditorElement {
header.child(
div()
.hover(|style| style.bg(colors.element_selected))
.rounded_xs()
.rounded_sm()
.child(
ButtonLike::new("toggle-buffer-fold")
.style(ui::ButtonStyle::Transparent)
@@ -4132,46 +4131,74 @@ impl EditorElement {
}
}
let mut paint_highlight =
|highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
let origin = point(
layout.hitbox.origin.x,
layout.hitbox.origin.y
+ (highlight_row_start.as_f32() - scroll_top)
* layout.position_map.line_height,
);
let size = size(
layout.hitbox.size.width,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
window.paint_quad(fill(Bounds { origin, size }, color));
};
let mut paint_highlight = |highlight_row_start: DisplayRow,
highlight_row_end: DisplayRow,
highlight: crate::LineHighlight,
edges| {
let origin = point(
layout.hitbox.origin.x,
layout.hitbox.origin.y
+ (highlight_row_start.as_f32() - scroll_top)
* layout.position_map.line_height,
);
let size = size(
layout.hitbox.size.width,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
let mut quad = fill(Bounds { origin, size }, highlight.background);
if let Some(border_color) = highlight.border {
quad.border_color = border_color;
quad.border_widths = edges
}
window.paint_quad(quad);
};
let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
None;
for (&new_row, &new_background) in &layout.highlighted_rows {
match &mut current_paint {
Some((current_background, current_range)) => {
Some((current_background, current_range, mut edges)) => {
let current_background = *current_background;
let new_range_started = current_background != new_background
|| current_range.end.next_row() != new_row;
if new_range_started {
if current_range.end.next_row() == new_row {
edges.bottom = px(0.);
};
paint_highlight(
current_range.start,
current_range.end,
current_background,
edges,
);
current_paint = Some((new_background, new_row..new_row));
let edges = Edges {
top: if current_range.end.next_row() != new_row {
px(1.)
} else {
px(0.)
},
bottom: px(1.),
..Default::default()
};
current_paint = Some((new_background, new_row..new_row, edges));
continue;
} else {
current_range.end = current_range.end.next_row();
}
}
None => current_paint = Some((new_background, new_row..new_row)),
None => {
let edges = Edges {
top: px(1.),
bottom: px(1.),
..Default::default()
};
current_paint = Some((new_background, new_row..new_row, edges))
}
};
}
if let Some((color, range)) = current_paint {
paint_highlight(range.start, range.end, color);
if let Some((color, range, edges)) = current_paint {
paint_highlight(range.start, range.end, color, edges);
}
let scroll_left =
@@ -4347,8 +4374,6 @@ impl EditorElement {
}
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let is_light = cx.theme().appearance().is_light();
if layout.display_hunks.is_empty() {
return;
}
@@ -4409,14 +4434,7 @@ impl EditorElement {
}),
};
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
hunk_to_paint
{
if secondary_status.has_secondary_hunk() {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
if let Some((hunk_bounds, background_color, corner_radii, _)) = 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
@@ -6734,10 +6752,6 @@ impl Element for EditorElement {
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let is_light = cx.theme().appearance().is_light();
let use_pattern = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
@@ -6758,25 +6772,27 @@ impl Element for EditorElement {
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let staged_background =
solid_background(background_color.opacity(hunk_opacity));
let unstaged_background = if use_pattern {
pattern_slash(
background_color.opacity(hunk_opacity),
window.rem_size().0 * 1.125, // ~18 by default
)
} else {
solid_background(background_color.opacity(if is_light {
let staged_highlight = LineHighlight {
background: (background_color.opacity(if is_light {
0.08
} else {
0.04
0.06
}))
.into(),
border: Some(if is_light {
background_color.opacity(0.48)
} else {
background_color.opacity(0.36)
}),
};
let unstaged_highlight =
solid_background(background_color.opacity(hunk_opacity)).into();
let background = if unstaged {
unstaged_background
unstaged_highlight
} else {
staged_background
staged_highlight
};
highlighted_rows
@@ -7625,7 +7641,7 @@ pub struct EditorLayout {
indent_guides: Option<Vec<IndentGuideLayout>>,
visible_display_row_range: Range<DisplayRow>,
active_rows: BTreeMap<DisplayRow, bool>,
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
line_elements: SmallVec<[AnyElement; 1]>,
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
@@ -8797,14 +8813,16 @@ fn diff_hunk_controls(
.h(line_height)
.mr_1()
.gap_1()
.px_1()
.px_0p5()
.pb_1()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.rounded_b_lg()
.bg(cx.theme().colors().editor_background)
.gap_1()
.occlude()
.shadow_md()
.child(if status.has_secondary_hunk() {
Button::new(("stage", row as u64), "Stage")
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
@@ -8861,7 +8879,7 @@ fn diff_hunk_controls(
})
})
.child(
Button::new("discard", "Restore")
Button::new("restore", "Restore")
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
@@ -8913,7 +8931,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
@@ -8949,7 +8967,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
Direction::Prev,

View File

@@ -370,7 +370,6 @@ impl GitBlame {
async move {
let Some(Blame {
entries,
permalinks,
messages,
remote_url,
}) = blame.await?
@@ -379,13 +378,8 @@ impl GitBlame {
};
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details = parse_commit_messages(
messages,
remote_url,
&permalinks,
provider_registry,
)
.await;
let commit_details =
parse_commit_messages(messages, remote_url, provider_registry).await;
anyhow::Ok(Some((entries, commit_details)))
}
@@ -477,7 +471,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> HashMap<Oid, ParsedCommitMessage> {
let mut commit_details = HashMap::default();
@@ -495,11 +488,7 @@ async fn parse_commit_messages(
},
))
} else {
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing. This is here for backwards compatibility, so that
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
// we fall back to that.
deprecated_permalinks.get(&oid).cloned()
continue;
};
let remote = parsed_remote_url

View File

@@ -241,10 +241,8 @@ impl Editor {
}
})
.collect();
let navigate_task =
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
self.select(SelectPhase::End, window, cx);
return navigate_task;
return self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
}
}
@@ -260,7 +258,7 @@ impl Editor {
cx,
);
let navigate_task = if point.as_valid().is_some() {
if point.as_valid().is_some() {
if modifiers.shift {
self.go_to_type_definition(&GoToTypeDefinition, window, cx)
} else {
@@ -268,9 +266,7 @@ impl Editor {
}
} else {
Task::ready(Ok(Navigated::No))
};
self.select(SelectPhase::End, window, cx);
return navigate_task;
}
}
}

View File

@@ -618,12 +618,12 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
},
syntax: cx.theme().syntax().clone(),
selection_background_color: { cx.theme().players().local().selection },
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
.text_base()
.mt(rems(1.))
.mb_0(),
..Default::default()
}
}

View File

@@ -622,8 +622,11 @@ impl Item for Editor {
ItemSettings::get_global(cx)
.file_icons
.then(|| {
path_for_buffer(&self.buffer, 0, true, cx)
.and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
self.buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).project_path(cx))
.and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
})
.flatten()
.map(Icon::from_path)
@@ -1601,13 +1604,11 @@ impl SearchableItem for Editor {
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Range<Anchor>],
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
active_match_index(
direction,
matches,
&self.selections.newest_anchor().head(),
&self.buffer().read(cx).snapshot(cx),
@@ -1620,7 +1621,6 @@ impl SearchableItem for Editor {
}
pub fn active_match_index(
direction: Direction,
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
@@ -1628,7 +1628,7 @@ pub fn active_match_index(
if ranges.is_empty() {
None
} else {
let r = ranges.binary_search_by(|probe| {
match ranges.binary_search_by(|probe| {
if probe.end.cmp(cursor, buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(cursor, buffer).is_gt() {
@@ -1636,15 +1636,8 @@ pub fn active_match_index(
} else {
Ordering::Equal
}
});
match direction {
Direction::Prev => match r {
Ok(i) => Some(i),
Err(i) => Some(i.saturating_sub(1)),
},
Direction::Next => match r {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
},
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
}
}
}

View File

@@ -1,616 +0,0 @@
use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
use gpui::{Context, Entity, Window};
use multi_buffer::{MultiBuffer, ToOffset};
use std::ops::Range;
use util::ResultExt as _;
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
use text::{Anchor, OffsetRangeExt as _};
use crate::Editor;
pub struct JsxTagCompletionState {
edit_index: usize,
open_tag_range: Range<usize>,
}
/// Index of the named child within an open or close tag
/// that corresponds to the tag name
/// Note that this is not configurable, i.e. we assume the first
/// named child of a tag node is the tag name
const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
/// Maximum number of parent elements to walk back when checking if an open tag
/// is already closed.
///
/// See the comment in `generate_auto_close_edits` for more details
const ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT: usize = 2;
pub(crate) fn should_auto_close(
buffer: &BufferSnapshot,
edited_ranges: &[Range<usize>],
config: &JsxTagAutoCloseConfig,
) -> Option<Vec<JsxTagCompletionState>> {
let mut to_auto_edit = vec![];
for (index, edited_range) in edited_ranges.iter().enumerate() {
let text = buffer
.text_for_range(edited_range.clone())
.collect::<String>();
if !text.ends_with(">") {
continue;
}
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
continue;
};
let Some(node) = layer
.node()
.named_descendant_for_byte_range(edited_range.start, edited_range.end)
else {
continue;
};
let mut jsx_open_tag_node = node;
if node.grammar_name() != config.open_tag_node_name {
if let Some(parent) = node.parent() {
if parent.grammar_name() == config.open_tag_node_name {
jsx_open_tag_node = parent;
}
}
}
if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
continue;
}
let first_two_chars: Option<[char; 2]> = {
let mut chars = buffer
.text_for_range(jsx_open_tag_node.byte_range())
.flat_map(|chunk| chunk.chars());
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
Some([c1, c2])
} else {
None
}
};
if let Some(chars) = first_two_chars {
if chars[0] != '<' {
continue;
}
if chars[1] == '!' || chars[1] == '/' {
continue;
}
}
to_auto_edit.push(JsxTagCompletionState {
edit_index: index,
open_tag_range: jsx_open_tag_node.byte_range(),
});
}
if to_auto_edit.is_empty() {
return None;
} else {
return Some(to_auto_edit);
}
}
pub(crate) fn generate_auto_close_edits(
buffer: &BufferSnapshot,
ranges: &[Range<usize>],
config: &JsxTagAutoCloseConfig,
state: Vec<JsxTagCompletionState>,
) -> Result<Vec<(Range<Anchor>, String)>> {
let mut edits = Vec::with_capacity(state.len());
for auto_edit in state {
let edited_range = ranges[auto_edit.edit_index].clone();
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
continue;
};
let layer_root_node = layer.node();
let Some(open_tag) = layer_root_node.descendant_for_byte_range(
auto_edit.open_tag_range.start,
auto_edit.open_tag_range.end,
) else {
continue;
};
assert!(open_tag.kind() == config.open_tag_node_name);
let tag_name = open_tag
.named_child(TS_NODE_TAG_NAME_CHILD_INDEX)
.filter(|node| node.kind() == config.tag_name_node_name)
.map_or("".to_string(), |node| {
buffer.text_for_range(node.byte_range()).collect::<String>()
});
/*
* Naive check to see if the tag is already closed
* Essentially all we do is count the number of open and close tags
* with the same tag name as the open tag just entered by the user
* The search is limited to some scope determined by
* `ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT`
*
* The limit is preferable to walking up the tree until we find a non-tag node,
* and then checking the entire tree, as this is unnecessarily expensive, and
* risks false positives
* eg. a `</div>` tag without a corresponding opening tag exists 25 lines away
* and the user typed in `<div>`, intuitively we still want to auto-close it because
* the other `</div>` tag is almost certainly not supposed to be the closing tag for the
* current element
*
* We have to walk up the tree some amount because tree-sitters error correction is not
* designed to handle this case, and usually does not represent the tree structure
* in the way we might expect,
*
* We half to walk up the tree until we hit an element with a different open tag name (`doing_deep_search == true`)
* because tree-sitter may pair the new open tag with the root of the tree's closing tag leaving the
* root's opening tag unclosed.
* e.g
* ```
* <div>
* <div>|cursor here|
* </div>
* ```
* in Astro/vue/svelte tree-sitter represented the tree as
* (
* (jsx_element
* (jsx_opening_element
* "<div>")
* )
* (jsx_element
* (jsx_opening_element
* "<div>") // <- cursor is here
* (jsx_closing_element
* "</div>")
* )
* )
* so if we only walked to the first `jsx_element` node,
* we would mistakenly identify the div entered by the
* user as already being closed, despite this clearly
* being false
*
* The errors with the tree-sitter tree caused by error correction,
* are also why the naive algorithm was chosen, as the alternative
* approach would be to maintain or construct a full parse tree (like tree-sitter)
* that better represents errors in a way that we can simply check
* the enclosing scope of the entered tag for a closing tag
* This is far more complex and expensive, and was deemed impractical
* given that the naive algorithm is sufficient in the majority of cases.
*/
{
let tag_node_name_equals = |node: &Node, tag_name_node_name: &str, name: &str| {
let is_empty = name.len() == 0;
if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
if node_name.kind() != tag_name_node_name {
return is_empty;
}
let range = node_name.byte_range();
return buffer.text_for_range(range).equals_str(name);
}
return is_empty;
};
let tree_root_node = {
let mut ancestors = Vec::with_capacity(
// estimate of max, not based on any data,
// but trying to avoid excessive reallocation
16,
);
ancestors.push(layer_root_node);
let mut cur = layer_root_node;
// walk down the tree until we hit the open tag
// note: this is what node.parent() does internally
while let Some(descendant) = cur.child_with_descendant(open_tag) {
if descendant == open_tag {
break;
}
ancestors.push(descendant);
cur = descendant;
}
assert!(ancestors.len() > 0);
let mut tree_root_node = open_tag;
let mut parent_element_node_count = 0;
let mut doing_deep_search = false;
for &ancestor in ancestors.iter().rev() {
tree_root_node = ancestor;
let is_element = ancestor.kind() == config.jsx_element_node_name;
let is_error = ancestor.is_error();
if is_error || !is_element {
break;
}
if is_element {
let is_first = parent_element_node_count == 0;
if !is_first {
let has_open_tag_with_same_tag_name = ancestor
.named_child(0)
.filter(|n| n.kind() == config.open_tag_node_name)
.map_or(false, |element_open_tag_node| {
tag_node_name_equals(
&element_open_tag_node,
&config.tag_name_node_name,
&tag_name,
)
});
if has_open_tag_with_same_tag_name {
doing_deep_search = true;
} else if doing_deep_search {
break;
}
}
parent_element_node_count += 1;
if !doing_deep_search
&& parent_element_node_count
>= ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT
{
break;
}
}
}
tree_root_node
};
let mut unclosed_open_tag_count: i32 = 0;
let mut cursor = layer_root_node.walk();
let mut stack = Vec::with_capacity(tree_root_node.descendant_count());
stack.extend(tree_root_node.children(&mut cursor));
let mut has_erroneous_close_tag = false;
let mut erroneous_close_tag_node_name = "";
let mut erroneous_close_tag_name_node_name = "";
if let Some(name) = config.erroneous_close_tag_node_name.as_deref() {
has_erroneous_close_tag = true;
erroneous_close_tag_node_name = name;
erroneous_close_tag_name_node_name = config
.erroneous_close_tag_name_node_name
.as_deref()
.unwrap_or(&config.tag_name_node_name);
}
let is_after_open_tag = |node: &Node| {
return node.start_byte() < open_tag.start_byte()
&& node.end_byte() < open_tag.start_byte();
};
// perf: use cursor for more efficient traversal
// if child -> go to child
// else if next sibling -> go to next sibling
// else -> go to parent
// if parent == tree_root_node -> break
while let Some(node) = stack.pop() {
let kind = node.kind();
if kind == config.open_tag_node_name {
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
unclosed_open_tag_count += 1;
}
} else if kind == config.close_tag_node_name {
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
unclosed_open_tag_count -= 1;
}
} else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
if tag_node_name_equals(&node, erroneous_close_tag_name_node_name, &tag_name) {
if !is_after_open_tag(&node) {
unclosed_open_tag_count -= 1;
}
}
} else if kind == config.jsx_element_node_name {
// perf: filter only open,close,element,erroneous nodes
stack.extend(node.children(&mut cursor));
}
}
if unclosed_open_tag_count <= 0 {
// skip if already closed
continue;
}
}
let edit_anchor = buffer.anchor_after(edited_range.end);
let edit_range = edit_anchor..edit_anchor;
edits.push((edit_range, format!("</{}>", tag_name)));
}
return Ok(edits);
}
pub(crate) fn refresh_enabled_in_any_buffer(
editor: &mut Editor,
multi_buffer: &Entity<MultiBuffer>,
cx: &Context<Editor>,
) {
editor.jsx_tag_auto_close_enabled_in_any_buffer = {
let multi_buffer = multi_buffer.read(cx);
let mut found_enabled = false;
multi_buffer.for_each_buffer(|buffer| {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
for syntax_layer in snapshot.syntax_layers() {
let language = syntax_layer.language;
if language.config().jsx_tag_auto_close.is_none() {
continue;
}
let language_settings = language::language_settings::language_settings(
Some(language.name()),
snapshot.file(),
cx,
);
if language_settings.jsx_tag_auto_close.enabled {
found_enabled = true;
}
}
});
found_enabled
};
}
pub(crate) type InitialBufferVersionsMap = HashMap<language::BufferId, clock::Global>;
pub(crate) fn construct_initial_buffer_versions_map<
D: ToOffset + Copy,
_S: Into<std::sync::Arc<str>>,
>(
editor: &Editor,
edits: &[(Range<D>, _S)],
cx: &Context<Editor>,
) -> InitialBufferVersionsMap {
let mut initial_buffer_versions = InitialBufferVersionsMap::default();
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
return initial_buffer_versions;
}
for (edit_range, _) in edits {
let edit_range_buffer = editor
.buffer()
.read(cx)
.excerpt_containing(edit_range.end, cx)
.map(|e| e.1);
if let Some(buffer) = edit_range_buffer {
let (buffer_id, buffer_version) =
buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version.clone()));
initial_buffer_versions.insert(buffer_id, buffer_version);
}
}
return initial_buffer_versions;
}
pub(crate) fn handle_from(
editor: &Editor,
initial_buffer_versions: InitialBufferVersionsMap,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
return;
}
struct JsxAutoCloseEditContext {
buffer: Entity<language::Buffer>,
config: language::JsxTagAutoCloseConfig,
edits: Vec<Range<usize>>,
}
let mut edit_contexts =
HashMap::<(language::BufferId, language::LanguageId), JsxAutoCloseEditContext>::default();
for (buffer_id, buffer_version_initial) in initial_buffer_versions {
let Some(buffer) = editor.buffer.read(cx).buffer(buffer_id) else {
continue;
};
let snapshot = buffer.read(cx).snapshot();
for edit in buffer.read(cx).edits_since(&buffer_version_initial) {
let Some(language) = snapshot.language_at(edit.new.end) else {
continue;
};
let Some(config) = language.config().jsx_tag_auto_close.as_ref() else {
continue;
};
let language_settings = snapshot.settings_at(edit.new.end, cx);
if !language_settings.jsx_tag_auto_close.enabled {
continue;
}
edit_contexts
.entry((snapshot.remote_id(), language.id()))
.or_insert_with(|| JsxAutoCloseEditContext {
buffer: buffer.clone(),
config: config.clone(),
edits: vec![],
})
.edits
.push(edit.new);
}
}
for ((buffer_id, _), auto_close_context) in edit_contexts {
let JsxAutoCloseEditContext {
buffer,
config: jsx_tag_auto_close_config,
edits: edited_ranges,
} = auto_close_context;
let (buffer_version_initial, mut buffer_parse_status_rx) =
buffer.read_with(cx, |buffer, _| (buffer.version(), buffer.parse_status()));
cx.spawn_in(window, |this, mut cx| async move {
let Some(buffer_parse_status) = buffer_parse_status_rx.recv().await.ok() else {
return Some(());
};
if buffer_parse_status == language::ParseStatus::Parsing {
let Some(language::ParseStatus::Idle) = buffer_parse_status_rx.recv().await.ok()
else {
return Some(());
};
}
let buffer_snapshot = buffer.read_with(&cx, |buf, _| buf.snapshot()).ok()?;
let Some(edit_behavior_state) =
should_auto_close(&buffer_snapshot, &edited_ranges, &jsx_tag_auto_close_config)
else {
return Some(());
};
let ensure_no_edits_since_start = || -> Option<()> {
// <div>wef,wefwef
let has_edits_since_start = this
.read_with(&cx, |this, cx| {
this.buffer.read_with(cx, |buffer, cx| {
buffer.buffer(buffer_id).map_or(true, |buffer| {
buffer.read_with(cx, |buffer, _| {
buffer.has_edits_since(&buffer_version_initial)
})
})
})
})
.ok()?;
if has_edits_since_start {
Err(anyhow!(
"Auto-close Operation Failed - Buffer has edits since start"
))
.log_err()?;
}
Some(())
};
ensure_no_edits_since_start()?;
let edits = cx
.background_executor()
.spawn({
let buffer_snapshot = buffer_snapshot.clone();
async move {
generate_auto_close_edits(
&buffer_snapshot,
&edited_ranges,
&jsx_tag_auto_close_config,
edit_behavior_state,
)
}
})
.await;
let edits = edits
.context("Auto-close Operation Failed - Failed to compute edits")
.log_err()?;
if edits.is_empty() {
return Some(());
}
// check again after awaiting background task before applying edits
ensure_no_edits_since_start()?;
let multi_buffer_snapshot = this
.read_with(&cx, |this, cx| {
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
})
.ok()?;
let mut base_selections = Vec::new();
let mut buffer_selection_map = HashMap::default();
{
let selections = this
.read_with(&cx, |this, _| this.selections.disjoint_anchors().clone())
.ok()?;
for selection in selections.iter() {
let Some(selection_buffer_offset_head) =
multi_buffer_snapshot.point_to_buffer_offset(selection.head())
else {
base_selections.push(selection.clone());
continue;
};
let Some(selection_buffer_offset_tail) =
multi_buffer_snapshot.point_to_buffer_offset(selection.tail())
else {
base_selections.push(selection.clone());
continue;
};
let is_entirely_in_buffer = selection_buffer_offset_head.0.remote_id()
== buffer_id
&& selection_buffer_offset_tail.0.remote_id() == buffer_id;
if !is_entirely_in_buffer {
base_selections.push(selection.clone());
continue;
}
let selection_buffer_offset_head = selection_buffer_offset_head.1;
let selection_buffer_offset_tail = selection_buffer_offset_tail.1;
buffer_selection_map.insert(
(selection_buffer_offset_head, selection_buffer_offset_tail),
(selection.clone(), None),
);
}
}
let mut any_selections_need_update = false;
for edit in &edits {
let edit_range_offset = edit.0.to_offset(&buffer_snapshot);
if edit_range_offset.start != edit_range_offset.end {
continue;
}
if let Some(selection) =
buffer_selection_map.get_mut(&(edit_range_offset.start, edit_range_offset.end))
{
if selection.0.head().bias() != text::Bias::Right
|| selection.0.tail().bias() != text::Bias::Right
{
continue;
}
if selection.1.is_none() {
any_selections_need_update = true;
selection.1 = Some(
selection
.0
.clone()
.map(|anchor| multi_buffer_snapshot.anchor_before(anchor)),
);
}
}
}
buffer
.update(&mut cx, |buffer, cx| {
buffer.edit(edits, None, cx);
})
.ok()?;
if any_selections_need_update {
let multi_buffer_snapshot = this
.read_with(&cx, |this, cx| {
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
})
.ok()?;
base_selections.extend(buffer_selection_map.values().map(|selection| {
match &selection.1 {
Some(left_biased_selection) => left_biased_selection.clone(),
None => selection.0.clone(),
}
}));
let base_selections = base_selections
.into_iter()
.map(|selection| {
selection.map(|anchor| anchor.to_offset(&multi_buffer_snapshot))
})
.collect::<Vec<_>>();
this.update_in(&mut cx, |this, window, cx| {
this.change_selections_inner(None, false, window, cx, |s| {
s.select(base_selections);
});
})
.ok()?;
}
Some(())
})
.detach();
}
}

View File

@@ -4,7 +4,7 @@ use anyhow::Context as _;
use gpui::{App, AppContext as _, Context, Entity, Window};
use language::{Capability, Language};
use multi_buffer::MultiBuffer;
use project::lsp_store::{lsp_ext_command::ExpandMacro, rust_analyzer_ext::RUST_ANALYZER_NAME};
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{
@@ -12,6 +12,8 @@ use crate::{
ExpandMacroRecursively, OpenDocs,
};
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
fn is_rust_language(language: &Language) -> bool {
language.name() == "Rust".into()
}
@@ -129,7 +131,7 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_store::lsp_ext_command::OpenDocs { position },
project::lsp_ext_command::OpenDocs { position },
cx,
)
});

View File

@@ -22,7 +22,6 @@ collections.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
fs.workspace = true
git.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true

View File

@@ -5,7 +5,6 @@ use client::{Client, UserStore};
use clock::RealSystemClock;
use collections::BTreeMap;
use feature_flags::FeatureFlagAppExt as _;
use git::GitHostingProviderRegistry;
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
use http_client::{HttpClient, Method};
use language::LanguageRegistry;
@@ -274,8 +273,7 @@ async fn run_evaluation(
let repos_dir = Path::new(EVAL_REPOS_DIR);
let db_path = Path::new(EVAL_DB_PATH);
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, bail, Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use collections::{BTreeMap, HashMap};
use fs::Fs;
use language::LanguageName;
@@ -85,61 +85,6 @@ pub struct ExtensionManifest {
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
#[serde(default)]
pub snippets: Option<PathBuf>,
#[serde(default)]
pub capabilities: Vec<ExtensionCapability>,
}
impl ExtensionManifest {
pub fn allow_exec(
&self,
desired_command: &str,
desired_args: &[impl AsRef<str> + std::fmt::Debug],
) -> Result<()> {
let is_allowed = self.capabilities.iter().any(|capability| match capability {
ExtensionCapability::ProcessExec { command, args } if command == desired_command => {
for (ix, arg) in args.iter().enumerate() {
if arg == "**" {
return true;
}
if ix >= desired_args.len() {
return false;
}
if arg != "*" && arg != desired_args[ix].as_ref() {
return false;
}
}
if args.len() < desired_args.len() {
return false;
}
true
}
_ => false,
});
if !is_allowed {
bail!(
"capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
);
}
Ok(())
}
}
/// A capability for an extension.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum ExtensionCapability {
#[serde(rename = "process:exec")]
ProcessExec {
/// The command to execute.
command: String,
/// The arguments to pass to the command. Use `*` for a single wildcard argument.
/// If the last element is `**`, then any trailing arguments are allowed.
args: Vec<String>,
},
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
@@ -273,104 +218,5 @@ fn manifest_from_old_manifest(
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extension_manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test".into(),
name: "Test".to_string(),
version: "1.0.0".into(),
schema_version: SchemaVersion::ZERO,
description: None,
repository: None,
authors: vec![],
lib: Default::default(),
themes: vec![],
icon_themes: vec![],
languages: vec![],
grammars: BTreeMap::default(),
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![],
}
}
#[test]
fn test_allow_exact_match() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec {
command: "ls".to_string(),
args: vec!["-la".to_string()],
}],
..extension_manifest()
};
assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
assert!(manifest.allow_exec("ls", &["-l"]).is_err());
assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
}
#[test]
fn test_allow_wildcard_arg() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec {
command: "git".to_string(),
args: vec!["*".to_string()],
}],
..extension_manifest()
};
assert!(manifest.allow_exec("git", &["status"]).is_ok());
assert!(manifest.allow_exec("git", &["commit"]).is_ok());
assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
}
#[test]
fn test_allow_double_wildcard() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec {
command: "cargo".to_string(),
args: vec!["test".to_string(), "**".to_string()],
}],
..extension_manifest()
};
assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
assert!(manifest
.allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
.is_ok());
assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
}
#[test]
fn test_allow_mixed_wildcards() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec {
command: "docker".to_string(),
args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
}],
..extension_manifest()
};
assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
assert!(manifest.allow_exec("docker", &["run"]).is_err());
assert!(manifest
.allow_exec("docker", &["run", "ubuntu", "bash"])
.is_ok());
assert!(manifest
.allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
.is_ok());
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
}
}

View File

@@ -163,7 +163,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
}),
dev: false,
},
@@ -192,7 +191,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
}),
dev: false,
},
@@ -358,7 +356,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
}),
dev: false,
},

View File

@@ -592,8 +592,6 @@ impl process::Host for WasmState {
command: process::Command,
) -> wasmtime::Result<Result<process::Output, String>> {
maybe!(async {
self.manifest.allow_exec(&command.command, &command.args)?;
let output = util::command::new_smol_command(command.command.as_str())
.args(&command.args)
.envs(command.env)

View File

@@ -40,7 +40,7 @@ impl RenderOnce for ExtensionCard {
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.rounded_md()
.children(self.children)
.when(self.overridden_by_dev_extension, |card| {
card.child(

View File

@@ -632,7 +632,7 @@ impl ExtensionsPage {
.px_0p5()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.rounded_md()
.child(
Label::new(label).size(LabelSize::XSmall),
)

View File

@@ -80,11 +80,6 @@ impl FeatureFlag for PredictEditsNonEagerModeFeatureFlag {
}
}
pub struct GitUiFeatureFlag;
impl FeatureFlag for GitUiFeatureFlag {
const NAME: &'static str = "git-ui";
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";

View File

@@ -470,7 +470,7 @@ impl Render for FeedbackModal {
.bg(cx.theme().colors().editor_background)
.p_2()
.border_1()
.rounded_sm()
.rounded_md()
.border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()),
)
@@ -482,7 +482,7 @@ impl Render for FeedbackModal {
.bg(cx.theme().colors().editor_background)
.p_2()
.border_1()
.rounded_sm()
.rounded_md()
.border_color(if self.valid_email_address() {
cx.theme().colors().border
} else {

View File

@@ -1,7 +1,5 @@
#[cfg(test)]
mod file_finder_tests;
#[cfg(test)]
mod open_path_prompt_tests;
pub mod file_finder_settings;
mod new_path_prompt;

View File

@@ -436,8 +436,8 @@ impl PickerDelegate for NewPathDelegate {
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
"Type a path...".into()
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Type a path...".into())
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {

View File

@@ -3,7 +3,7 @@ use fuzzy::StringMatchCandidate;
use picker::{Picker, PickerDelegate};
use project::DirectoryLister;
use std::{
path::{Path, PathBuf, MAIN_SEPARATOR_STR},
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
@@ -38,38 +38,14 @@ impl OpenPathDelegate {
should_dismiss: true,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn collect_match_candidates(&self) -> Vec<String> {
if let Some(state) = self.directory_state.as_ref() {
self.matches
.iter()
.filter_map(|&index| {
state
.match_candidates
.get(index)
.map(|candidate| candidate.path.string.clone())
})
.collect()
} else {
Vec::new()
}
}
}
#[derive(Debug)]
struct DirectoryState {
path: String,
match_candidates: Vec<CandidateInfo>,
match_candidates: Vec<StringMatchCandidate>,
error: Option<SharedString>,
}
#[derive(Debug, Clone)]
struct CandidateInfo {
path: StringMatchCandidate,
is_dir: bool,
}
impl OpenPathPrompt {
pub(crate) fn register(
workspace: &mut Workspace,
@@ -117,6 +93,8 @@ impl PickerDelegate for OpenPathDelegate {
cx.notify();
}
// todo(windows)
// Is this method woring correctly on Windows? This method uses `/` for path separator.
fn update_matches(
&mut self,
query: String,
@@ -124,26 +102,13 @@ impl PickerDelegate for OpenPathDelegate {
cx: &mut Context<Picker<Self>>,
) -> gpui::Task<()> {
let lister = self.lister.clone();
let query_path = Path::new(&query);
let last_item = query_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(&last_item) {
(dir.to_string(), last_item)
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
(query[..index].to_string(), query[index + 1..].to_string())
} else {
(query, String::new())
};
if dir == "" {
#[cfg(not(target_os = "windows"))]
{
dir = "/".to_string();
}
#[cfg(target_os = "windows")]
{
dir = "C:\\".to_string();
}
dir = "/".to_string();
}
let query = if self
@@ -169,16 +134,12 @@ impl PickerDelegate for OpenPathDelegate {
this.update(&mut cx, |this, _| {
this.delegate.directory_state = Some(match paths {
Ok(mut paths) => {
paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
let match_candidates = paths
.iter()
.enumerate()
.map(|(ix, item)| CandidateInfo {
path: StringMatchCandidate::new(
ix,
&item.path.to_string_lossy(),
),
is_dir: item.is_dir,
.map(|(ix, path)| {
StringMatchCandidate::new(ix, &path.to_string_lossy())
})
.collect::<Vec<_>>();
@@ -217,7 +178,7 @@ impl PickerDelegate for OpenPathDelegate {
};
if !suffix.starts_with('.') {
match_candidates.retain(|m| !m.path.string.starts_with('.'));
match_candidates.retain(|m| !m.string.starts_with('.'));
}
if suffix == "" {
@@ -225,7 +186,7 @@ impl PickerDelegate for OpenPathDelegate {
this.delegate.matches.clear();
this.delegate
.matches
.extend(match_candidates.iter().map(|m| m.path.id));
.extend(match_candidates.iter().map(|m| m.id));
cx.notify();
})
@@ -233,9 +194,8 @@ impl PickerDelegate for OpenPathDelegate {
return;
}
let candidates = match_candidates.iter().map(|m| &m.path).collect::<Vec<_>>();
let matches = fuzzy::match_strings(
candidates.as_slice(),
match_candidates.as_slice(),
&suffix,
false,
100,
@@ -257,7 +217,7 @@ impl PickerDelegate for OpenPathDelegate {
this.delegate.directory_state.as_ref().and_then(|d| {
d.match_candidates
.get(*m)
.map(|c| !c.path.string.starts_with(&suffix))
.map(|c| !c.string.starts_with(&suffix))
}),
*m,
)
@@ -279,16 +239,7 @@ impl PickerDelegate for OpenPathDelegate {
let m = self.matches.get(self.selected_index)?;
let directory_state = self.directory_state.as_ref()?;
let candidate = directory_state.match_candidates.get(*m)?;
Some(format!(
"{}{}{}",
directory_state.path,
candidate.path.string,
if candidate.is_dir {
MAIN_SEPARATOR_STR
} else {
""
}
))
Some(format!("{}/{}", directory_state.path, candidate.string))
})
.unwrap_or(query),
)
@@ -309,7 +260,7 @@ impl PickerDelegate for OpenPathDelegate {
.resolve_tilde(&directory_state.path, cx)
.as_ref(),
)
.join(&candidate.path.string);
.join(&candidate.string);
if let Some(tx) = self.tx.take() {
tx.send(Some(vec![result])).ok();
}
@@ -343,19 +294,21 @@ impl PickerDelegate for OpenPathDelegate {
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.child(LabelLike::new().child(candidate.path.string.clone())),
.child(LabelLike::new().child(candidate.string.clone())),
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
{
error
} else {
"No such file or directory".into()
}
};
Some(text)
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
Arc::from("[directory/]filename.ext")
}
}

View File

@@ -1,324 +0,0 @@
use std::sync::Arc;
use gpui::{AppContext, Entity, TestAppContext, VisualTestContext};
use picker::{Picker, PickerDelegate};
use project::Project;
use serde_json::json;
use ui::rems;
use util::path;
use workspace::{AppState, Workspace};
use crate::OpenPathDelegate;
#[gpui::test]
async fn test_open_path_prompt(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
path!("/root"),
json!({
"a1": "A1",
"a2": "A2",
"a3": "A3",
"dir1": {},
"dir2": {
"c": "C",
"d1": "D1",
"d2": "D2",
"d3": "D3",
"dir3": {},
"dir4": {}
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, cx);
let query = path!("/root");
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
// If the query ends with a slash, the picker should show the contents of the directory.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a1", "a2", "a3", "dir1", "dir2"]
);
// Show candidates for the query "a".
let query = path!("/root/a");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a1", "a2", "a3"]
);
// Show candidates for the query "d".
let query = path!("/root/d");
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
let query = path!("/root/dir2");
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir2"]);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["c", "d1", "d2", "d3", "dir3", "dir4"]
);
// Show candidates for the query "d".
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["d1", "d2", "d3", "dir3", "dir4"]
);
let query = path!("/root/dir2/di");
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir3", "dir4"]);
}
#[gpui::test]
async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
path!("/root"),
json!({
"a": "A",
"dir1": {},
"dir2": {
"c": "C",
"d": "D",
"dir3": {},
"dir4": {}
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, cx);
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/"));
// Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
// Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
path!("/root/dir1/")
);
let query = path!("/root/a");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
let query = path!("/root/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
path!("/root/dir2/")
);
let query = path!("/root/dir2");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
path!("/root/dir2/")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
path!("/root/dir2/c")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 2, &picker, cx),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
path!("/root/dir2/d")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/di");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
path!("/root/dir2/dir4/")
);
}
#[gpui::test]
#[cfg(target_os = "windows")]
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
path!("/root"),
json!({
"a": "A",
"dir1": {},
"dir2": {}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, cx);
// Support both forward and backward slashes.
let query = "C:/root/";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a");
let query = "C:\\root/";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a");
let query = "C:\\root\\";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a");
// Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "C:/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\");
let query = "C:\\root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\");
let query = "C:\\root\\d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(
confirm_completion(query, 0, &picker, cx),
"C:\\root\\dir1\\"
);
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
super::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
fn build_open_path_prompt(
project: Entity<Project>,
cx: &mut TestAppContext,
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
let (tx, _) = futures::channel::oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
let delegate = OpenPathDelegate::new(tx, lister.clone());
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
(
workspace.update_in(cx, |_, window, cx| {
cx.new(|cx| {
let picker = Picker::uniform_list(delegate, window, cx)
.width(rems(34.))
.modal(false);
let query = lister.default_query(cx);
picker.set_query(query, window, cx);
picker
})
}),
cx,
)
}
async fn insert_query(
query: &str,
picker: &Entity<Picker<OpenPathDelegate>>,
cx: &mut VisualTestContext,
) {
picker
.update_in(cx, |f, window, cx| {
f.delegate.update_matches(query.to_string(), window, cx)
})
.await;
}
fn confirm_completion(
query: &str,
select: usize,
picker: &Entity<Picker<OpenPathDelegate>>,
cx: &mut VisualTestContext,
) -> String {
picker
.update_in(cx, |f, window, cx| {
if f.delegate.selected_index() != select {
f.delegate.set_selected_index(select, window, cx);
}
f.delegate.confirm_completion(query.to_string(), window, cx)
})
.unwrap()
}
fn collect_match_candidates(
picker: &Entity<Picker<OpenPathDelegate>>,
cx: &mut VisualTestContext,
) -> Vec<String> {
picker.update(cx, |f, _| f.delegate.collect_match_candidates())
}

View File

@@ -11,18 +11,19 @@ use collections::HashMap;
use git::status::StatusCode;
#[cfg(any(test, feature = "test-support"))]
use git::status::TrackedStatus;
use git::GitHostingProviderRegistry;
#[cfg(any(test, feature = "test-support"))]
use git::{repository::RepoPath, status::FileStatus};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use ashpd::desktop::trash;
use std::borrow::Cow;
#[cfg(any(test, feature = "test-support"))]
use std::collections::HashSet;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
use std::os::fd::AsRawFd;
use util::command::new_std_command;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
@@ -137,6 +138,7 @@ pub trait Fs: Send + Sync {
fn home_dir(&self) -> Option<PathBuf>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
@@ -247,7 +249,6 @@ impl From<MTime> for proto::Timestamp {
#[derive(Default)]
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
}
@@ -300,14 +301,8 @@ impl FileHandle for std::fs::File {
pub struct RealWatcher {}
impl RealFs {
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
}
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
Self { git_binary_path }
}
}
@@ -770,10 +765,32 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
}
fn git_init(&self, abs_work_directory_path: &Path, fallback_branch_name: String) -> Result<()> {
let config = new_std_command("git")
.current_dir(abs_work_directory_path)
.args(&["config", "--global", "--get", "init.defaultBranch"])
.output()?;
let branch_name;
if config.status.success() && !config.stdout.is_empty() {
branch_name = String::from_utf8_lossy(&config.stdout);
} else {
branch_name = Cow::Borrowed(fallback_branch_name.as_str());
}
new_std_command("git")
.current_dir(abs_work_directory_path)
.args(&["init", "-b"])
.arg(branch_name.trim())
.output()?;
Ok(())
}
fn is_fake(&self) -> bool {
false
}
@@ -2084,6 +2101,14 @@ impl Fs for FakeFs {
}
}
fn git_init(
&self,
abs_work_directory_path: &Path,
_fallback_branch_name: String,
) -> Result<()> {
smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
}
fn is_fake(&self) -> bool {
true
}

View File

@@ -1,5 +1,5 @@
use std::{
borrow::{Borrow, Cow},
borrow::Cow,
sync::atomic::{self, AtomicBool},
};
@@ -50,24 +50,22 @@ impl<'a> Matcher<'a> {
/// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as
/// the input candidates.
pub fn match_candidates<C, R, F, T>(
pub fn match_candidates<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = T>,
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
C: MatchCandidate,
T: Borrow<C>,
F: Fn(&C, f64, &Vec<usize>) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.borrow().has_chars(self.query_char_bag) {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
@@ -77,7 +75,7 @@ impl<'a> Matcher<'a> {
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.borrow().to_string().chars() {
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
}
@@ -100,11 +98,7 @@ impl<'a> Matcher<'a> {
);
if score > 0.0 {
results.push(build_match(
candidate.borrow(),
score,
&self.match_positions,
));
results.push(build_match(&candidate, score, &self.match_positions));
}
}
}

View File

@@ -4,7 +4,7 @@ use crate::{
};
use gpui::BackgroundExecutor;
use std::{
borrow::{Borrow, Cow},
borrow::Cow,
cmp::{self, Ordering},
iter,
ops::Range,
@@ -113,17 +113,14 @@ impl Ord for StringMatch {
}
}
pub async fn match_strings<T>(
candidates: &[T],
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
executor: BackgroundExecutor,
) -> Vec<StringMatch>
where
T: Borrow<StringMatchCandidate> + Sync,
{
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
@@ -132,10 +129,10 @@ where
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.borrow().id,
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.borrow().string.clone(),
string: candidate.string.clone(),
})
.collect();
}
@@ -166,12 +163,10 @@ where
matcher.match_candidates(
&[],
&[],
candidates[segment_start..segment_end]
.iter()
.map(|c| c.borrow()),
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate: &&StringMatchCandidate, score, positions| StringMatch {
|candidate, score, positions| StringMatch {
candidate_id: candidate.id,
score,
positions: positions.clone(),

View File

@@ -1,17 +1,15 @@
use crate::commit::get_messages;
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
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::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
use time::macros::format_description;
use time::OffsetDateTime;
use time::UtcOffset;
use url::Url;
pub use git2 as libgit;
@@ -19,52 +17,34 @@ pub use git2 as libgit;
pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub permalinks: HashMap<Oid, Url>,
pub remote_url: Option<String>,
}
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>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> 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));
let mut permalinks = HashMap::default();
let mut unique_shas = HashSet::default();
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing.
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
permalinks.entry(entry.sha).or_insert_with(|| {
provider.build_commit_permalink(
remote,
BuildCommitPermalinkParams {
sha: entry.sha.to_string().as_str(),
},
)
});
}
}
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,
permalinks,
messages,
remote_url,
})
@@ -74,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")
@@ -93,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

@@ -50,11 +50,14 @@ actions!(
Fetch,
Commit,
ExpandCommitEditor,
GenerateCommitMessage
GenerateCommitMessage,
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

@@ -54,6 +54,39 @@ impl From<TrackedStatus> for FileStatus {
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StageStatus {
Staged,
Unstaged,
PartiallyStaged,
}
impl StageStatus {
pub fn is_fully_staged(&self) -> bool {
matches!(self, StageStatus::Staged)
}
pub fn is_fully_unstaged(&self) -> bool {
matches!(self, StageStatus::Unstaged)
}
pub fn has_staged(&self) -> bool {
matches!(self, StageStatus::Staged | StageStatus::PartiallyStaged)
}
pub fn has_unstaged(&self) -> bool {
matches!(self, StageStatus::Unstaged | StageStatus::PartiallyStaged)
}
pub fn as_bool(self) -> Option<bool> {
match self {
StageStatus::Staged => Some(true),
StageStatus::Unstaged => Some(false),
StageStatus::PartiallyStaged => None,
}
}
}
impl FileStatus {
pub const fn worktree(worktree_status: StatusCode) -> Self {
FileStatus::Tracked(TrackedStatus {
@@ -106,15 +139,15 @@ impl FileStatus {
Ok(status)
}
pub fn is_staged(self) -> Option<bool> {
pub fn staging(self) -> StageStatus {
match self {
FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
Some(false)
StageStatus::Unstaged
}
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
(StatusCode::Unmodified, _) => Some(false),
(_, StatusCode::Unmodified) => Some(true),
_ => None,
(StatusCode::Unmodified, _) => StageStatus::Unstaged,
(_, StatusCode::Unmodified) => StageStatus::Staged,
_ => StageStatus::PartiallyStaged,
},
}
}

View File

@@ -2,9 +2,12 @@ mod providers;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use git::repository::GitRepository;
use git::GitHostingProviderRegistry;
use gpui::App;
use url::Url;
use util::maybe;
pub use crate::providers::*;
@@ -15,7 +18,7 @@ pub fn init(cx: &App) {
provider_registry.register_hosting_provider(Arc::new(Chromium));
provider_registry.register_hosting_provider(Arc::new(Codeberg));
provider_registry.register_hosting_provider(Arc::new(Gitee));
provider_registry.register_hosting_provider(Arc::new(Github));
provider_registry.register_hosting_provider(Arc::new(Github::new()));
provider_registry.register_hosting_provider(Arc::new(Gitlab::new()));
provider_registry.register_hosting_provider(Arc::new(Sourcehut));
}
@@ -34,5 +37,51 @@ pub fn register_additional_providers(
if let Ok(gitlab_self_hosted) = Gitlab::from_remote_url(&origin_url) {
provider_registry.register_hosting_provider(Arc::new(gitlab_self_hosted));
} else if let Ok(github_self_hosted) = Github::from_remote_url(&origin_url) {
provider_registry.register_hosting_provider(Arc::new(github_self_hosted));
}
}
pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> {
maybe!({
if let Some(remote_url) = remote_url.strip_prefix("git@") {
if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
return Some(host.to_string());
}
}
Url::parse(&remote_url)
.ok()
.and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
})
.ok_or_else(|| anyhow!("URL has no host"))
}
#[cfg(test)]
mod tests {
use super::get_host_from_git_remote_url;
use pretty_assertions::assert_eq;
#[test]
fn test_get_host_from_git_remote_url() {
let tests = [
(
"https://jlannister@github.com/some-org/some-repo.git",
Some("github.com".to_string()),
),
(
"git@github.com:zed-industries/zed.git",
Some("github.com".to_string()),
),
(
"git@my.super.long.subdomain.com:zed-industries/zed.git",
Some("my.super.long.subdomain.com".to_string()),
),
];
for (remote_url, expected_host) in tests {
let host = get_host_from_git_remote_url(remote_url).ok();
assert_eq!(host, expected_host);
}
}
}

View File

@@ -15,6 +15,8 @@ use git::{
PullRequest, RemoteUrl,
};
use crate::get_host_from_git_remote_url;
fn pull_request_number_regex() -> &'static Regex {
static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\(#(\d+)\)$").unwrap());
@@ -43,9 +45,38 @@ struct User {
pub avatar_url: String,
}
pub struct Github;
pub struct Github {
name: String,
base_url: Url,
}
impl Github {
pub fn new() -> Self {
Self {
name: "GitHub".to_string(),
base_url: Url::parse("https://github.com").unwrap(),
}
}
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
let host = get_host_from_git_remote_url(remote_url)?;
if host == "github.com" {
bail!("the GitHub instance is not self-hosted");
}
// TODO: detecting self hosted instances by checking whether "github" is in the url or not
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
// information.
if !host.contains("github") {
bail!("not a GitHub URL");
}
Ok(Self {
name: "GitHub Self-Hosted".to_string(),
base_url: Url::parse(&format!("https://{}", host))?,
})
}
async fn fetch_github_commit_author(
&self,
repo_owner: &str,
@@ -53,7 +84,10 @@ impl Github {
commit: &str,
client: &Arc<dyn HttpClient>,
) -> Result<Option<User>> {
let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}");
let Some(host) = self.base_url.host_str() else {
bail!("failed to get host from github base url");
};
let url = format!("https://api.{host}/repos/{repo_owner}/{repo}/commits/{commit}");
let mut request = Request::get(&url)
.header("Content-Type", "application/json")
@@ -90,15 +124,17 @@ impl Github {
#[async_trait]
impl GitHostingProvider for Github {
fn name(&self) -> String {
"GitHub".to_string()
self.name.clone()
}
fn base_url(&self) -> Url {
Url::parse("https://github.com").unwrap()
self.base_url.clone()
}
fn supports_avatars(&self) -> bool {
true
// Avatars are not supported for self-hosted GitHub instances
// See tracking issue: https://github.com/zed-industries/zed/issues/11043
&self.name == "GitHub"
}
fn format_line_number(&self, line: u32) -> String {
@@ -113,7 +149,7 @@ impl GitHostingProvider for Github {
let url = RemoteUrl::from_str(url).ok()?;
let host = url.host_str()?;
if host != "github.com" {
if host != self.base_url.host_str()? {
return None;
}
@@ -203,9 +239,76 @@ mod tests {
use super::*;
#[test]
fn test_invalid_self_hosted_remote_url() {
let remote_url = "git@github.com:zed-industries/zed.git";
let github = Github::from_remote_url(remote_url);
assert!(github.is_err());
}
#[test]
fn test_from_remote_url_ssh() {
let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";
let github = Github::from_remote_url(remote_url).unwrap();
assert!(!github.supports_avatars());
assert_eq!(github.name, "GitHub Self-Hosted".to_string());
assert_eq!(
github.base_url,
Url::parse("https://github.my-enterprise.com").unwrap()
);
}
#[test]
fn test_from_remote_url_https() {
let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";
let github = Github::from_remote_url(remote_url).unwrap();
assert!(!github.supports_avatars());
assert_eq!(github.name, "GitHub Self-Hosted".to_string());
assert_eq!(
github.base_url,
Url::parse("https://github.my-enterprise.com").unwrap()
);
}
#[test]
fn test_parse_remote_url_given_self_hosted_ssh_url() {
let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";
let parsed_remote = Github::from_remote_url(remote_url)
.unwrap()
.parse_remote_url(remote_url)
.unwrap();
assert_eq!(
parsed_remote,
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
}
);
}
#[test]
fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() {
let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";
let parsed_remote = Github::from_remote_url(remote_url)
.unwrap()
.parse_remote_url(remote_url)
.unwrap();
assert_eq!(
parsed_remote,
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
}
);
}
#[test]
fn test_parse_remote_url_given_ssh_url() {
let parsed_remote = Github
let parsed_remote = Github::new()
.parse_remote_url("git@github.com:zed-industries/zed.git")
.unwrap();
@@ -220,7 +323,7 @@ mod tests {
#[test]
fn test_parse_remote_url_given_https_url() {
let parsed_remote = Github
let parsed_remote = Github::new()
.parse_remote_url("https://github.com/zed-industries/zed.git")
.unwrap();
@@ -235,7 +338,7 @@ mod tests {
#[test]
fn test_parse_remote_url_given_https_url_with_username() {
let parsed_remote = Github
let parsed_remote = Github::new()
.parse_remote_url("https://jlannister@github.com/some-org/some-repo.git")
.unwrap();
@@ -254,7 +357,7 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
};
let permalink = Github.build_permalink(
let permalink = Github::new().build_permalink(
remote,
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
@@ -269,7 +372,7 @@ mod tests {
#[test]
fn test_build_github_permalink() {
let permalink = Github.build_permalink(
let permalink = Github::new().build_permalink(
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
@@ -287,7 +390,7 @@ mod tests {
#[test]
fn test_build_github_permalink_with_single_line_selection() {
let permalink = Github.build_permalink(
let permalink = Github::new().build_permalink(
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
@@ -305,7 +408,7 @@ mod tests {
#[test]
fn test_build_github_permalink_with_multi_line_selection() {
let permalink = Github.build_permalink(
let permalink = Github::new().build_permalink(
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
@@ -328,8 +431,9 @@ mod tests {
repo: "zed".into(),
};
let github = Github::new();
let message = "This does not contain a pull request";
assert!(Github.extract_pull_request(&remote, message).is_none());
assert!(github.extract_pull_request(&remote, message).is_none());
// Pull request number at end of first line
let message = indoc! {r#"
@@ -344,7 +448,7 @@ mod tests {
};
assert_eq!(
Github
github
.extract_pull_request(&remote, &message)
.unwrap()
.url
@@ -359,6 +463,6 @@ mod tests {
See the original PR, this is a fix.
"#
};
assert_eq!(Github.extract_pull_request(&remote, &message), None);
assert_eq!(github.extract_pull_request(&remote, &message), None);
}
}

View File

@@ -1,14 +1,15 @@
use std::str::FromStr;
use anyhow::{anyhow, bail, Result};
use anyhow::{bail, Result};
use url::Url;
use util::maybe;
use git::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
RemoteUrl,
};
use crate::get_host_from_git_remote_url;
#[derive(Debug)]
pub struct Gitlab {
name: String,
@@ -24,19 +25,14 @@ impl Gitlab {
}
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
let host = maybe!({
if let Some(remote_url) = remote_url.strip_prefix("git@") {
if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
return Some(host.to_string());
}
}
Url::parse(&remote_url)
.ok()
.and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
})
.ok_or_else(|| anyhow!("URL has no host"))?;
let host = get_host_from_git_remote_url(remote_url)?;
if host == "gitlab.com" {
bail!("the GitLab instance is not self-hosted");
}
// TODO: detecting self hosted instances by checking whether "gitlab" is in the url or not
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
// information.
if !host.contains("gitlab") {
bail!("not a GitLab URL");
}
@@ -130,6 +126,13 @@ mod tests {
use super::*;
#[test]
fn test_invalid_self_hosted_remote_url() {
let remote_url = "https://gitlab.com/zed-industries/zed.git";
let github = Gitlab::from_remote_url(remote_url);
assert!(github.is_err());
}
#[test]
fn test_parse_remote_url_given_ssh_url() {
let parsed_remote = Gitlab::new()

View File

@@ -18,13 +18,15 @@ test-support = ["multi_buffer/test-support"]
[dependencies]
anyhow.workspace = true
askpass.workspace= true
askpass.workspace = true
assistant_settings.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
@@ -37,6 +39,7 @@ linkme.workspace = true
log.workspace = true
menu.workspace = true
multi_buffer.workspace = true
notifications.workspace = true
panel.workspace = true
picker.workspace = true
postage.workspace = true
@@ -51,6 +54,7 @@ strum.workspace = true
telemetry.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -74,7 +74,7 @@ impl Render for AskPassModal {
.px(DynamicSpacing::Base12.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.pb(DynamicSpacing::Base04.rems(cx))
.rounded_t_sm()
.rounded_t_md()
.w_full()
.gap_1p5()
.child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))

View File

@@ -1,16 +1,18 @@
use anyhow::{anyhow, Context as _};
use fuzzy::{StringMatch, StringMatchCandidate};
use fuzzy::StringMatchCandidate;
use git::repository::Branch;
use gpui::{
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, Window,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
SharedString, Styled, Subscription, Task, Window,
};
use picker::{Picker, PickerDelegate};
use project::git::Repository;
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
use time::OffsetDateTime;
use time_format::format_local_timestamp;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@@ -18,10 +20,30 @@ use workspace::{ModalView, Workspace};
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(open);
workspace.register_action(switch);
workspace.register_action(checkout_branch);
})
.detach();
}
pub fn checkout_branch(
workspace: &mut Workspace,
_: &zed_actions::git::CheckoutBranch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open(workspace, &zed_actions::git::Branch, window, cx);
}
pub fn switch(
workspace: &mut Workspace,
_: &zed_actions::git::Switch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open(workspace, &zed_actions::git::Branch, window, cx);
}
pub fn open(
workspace: &mut Workspace,
_: &zed_actions::git::Branch,
@@ -31,7 +53,7 @@ pub fn open(
let repository = workspace.project().read(cx).active_repository(cx).clone();
let style = BranchListStyle::Modal;
workspace.toggle_modal(window, cx, |window, cx| {
BranchList::new(repository, style, 34., window, cx)
BranchList::new(repository, style, rems(34.), window, cx)
})
}
@@ -41,7 +63,7 @@ pub fn popover(
cx: &mut App,
) -> Entity<BranchList> {
cx.new(|cx| {
let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx);
list.focus_handle(cx).focus(window);
list
})
@@ -54,8 +76,7 @@ enum BranchListStyle {
}
pub struct BranchList {
rem_width: f32,
pub popover_handle: PopoverMenuHandle<Self>,
width: Rems,
pub picker: Entity<Picker<BranchListDelegate>>,
_subscription: Subscription,
}
@@ -64,20 +85,26 @@ impl BranchList {
fn new(
repository: Option<Entity<Repository>>,
style: BranchListStyle,
rem_width: f32,
width: Rems,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let popover_handle = PopoverMenuHandle::default();
let all_branches_request = repository
.clone()
.map(|repository| repository.read(cx).branches());
cx.spawn_in(window, |this, mut cx| async move {
let all_branches = all_branches_request
let mut all_branches = all_branches_request
.context("No active repository")?
.await??;
all_branches.sort_by_key(|branch| {
branch
.most_recent_commit
.as_ref()
.map(|commit| 0 - commit.commit_timestamp)
});
this.update_in(&mut cx, |this, window, cx| {
this.picker.update(cx, |picker, cx| {
picker.delegate.all_branches = Some(all_branches);
@@ -89,7 +116,7 @@ impl BranchList {
})
.detach_and_log_err(cx);
let delegate = BranchListDelegate::new(repository.clone(), style, 20);
let delegate = BranchListDelegate::new(repository.clone(), style);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -98,11 +125,20 @@ impl BranchList {
Self {
picker,
rem_width,
popover_handle,
width,
_subscription,
}
}
fn handle_modifiers_changed(
&mut self,
ev: &ModifiersChangedEvent,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.picker
.update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
}
}
impl ModalView for BranchList {}
impl EventEmitter<DismissEvent> for BranchList {}
@@ -116,7 +152,8 @@ impl Focusable for BranchList {
impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(self.rem_width))
.w(self.width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.child(self.picker.clone())
.on_mouse_down_out({
cx.listener(move |this, _, window, cx| {
@@ -129,20 +166,10 @@ impl Render for BranchList {
}
#[derive(Debug, Clone)]
enum BranchEntry {
Branch(StringMatch),
History(String),
NewBranch { name: String },
}
impl BranchEntry {
fn name(&self) -> &str {
match self {
Self::Branch(branch) => &branch.string,
Self::History(branch) => &branch,
Self::NewBranch { name } => &name,
}
}
struct BranchEntry {
branch: Branch,
positions: Vec<usize>,
is_new: bool,
}
pub struct BranchListDelegate {
@@ -152,16 +179,11 @@ pub struct BranchListDelegate {
style: BranchListStyle,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
branch_name_trailoff_after: usize,
modifiers: Modifiers,
}
impl BranchListDelegate {
fn new(
repo: Option<Entity<Repository>>,
style: BranchListStyle,
branch_name_trailoff_after: usize,
) -> Self {
fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
Self {
matches: vec![],
repo,
@@ -169,15 +191,30 @@ impl BranchListDelegate {
all_branches: None,
selected_index: 0,
last_query: Default::default(),
branch_name_trailoff_after,
modifiers: Default::default(),
}
}
pub fn branch_count(&self) -> usize {
self.matches
.iter()
.filter(|item| matches!(item, BranchEntry::Branch(_)))
.count()
fn create_branch(
&self,
new_branch_name: SharedString,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let Some(repo) = self.repo.clone() else {
return;
};
cx.spawn(|_, cx| async move {
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.to_string()))?
.await??;
Ok(())
})
.detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| {
Some(e.to_string())
});
cx.emit(DismissEvent);
}
}
@@ -211,37 +248,28 @@ impl PickerDelegate for BranchListDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(mut all_branches) = self.all_branches.clone() else {
let Some(all_branches) = self.all_branches.clone() else {
return Task::ready(());
};
const RECENT_BRANCHES_COUNT: usize = 10;
cx.spawn_in(window, move |picker, mut cx| async move {
const RECENT_BRANCHES_COUNT: usize = 10;
if query.is_empty() {
if all_branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.priority_key().cmp(&lhs.priority_key())
});
all_branches.truncate(RECENT_BRANCHES_COUNT);
}
all_branches.sort_unstable_by(|lhs, rhs| {
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
});
}
let candidates = all_branches
.into_iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<StringMatchCandidate>>();
let matches: Vec<BranchEntry> = if query.is_empty() {
candidates
let mut matches: Vec<BranchEntry> = if query.is_empty() {
all_branches
.into_iter()
.map(|candidate| BranchEntry::History(candidate.string))
.take(RECENT_BRANCHES_COUNT)
.map(|branch| BranchEntry {
branch,
positions: Vec::new(),
is_new: false,
})
.collect()
} else {
let candidates = all_branches
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
.collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
&query,
@@ -253,20 +281,35 @@ impl PickerDelegate for BranchListDelegate {
.await
.iter()
.cloned()
.map(BranchEntry::Branch)
.map(|candidate| BranchEntry {
branch: all_branches[candidate.candidate_id].clone(),
positions: candidate.positions,
is_new: false,
})
.collect()
};
picker
.update(&mut cx, |picker, _| {
#[allow(clippy::nonminimal_bool)]
if !query.is_empty()
&& !matches
.first()
.is_some_and(|entry| entry.branch.name == query)
{
matches.push(BranchEntry {
branch: Branch {
name: query.clone().into(),
is_head: false,
upstream: None,
most_recent_commit: None,
},
positions: Vec::new(),
is_new: true,
})
}
let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
if !query.is_empty() {
delegate.matches.push(BranchEntry::NewBranch {
name: query.trim().replace(' ', "-"),
});
}
delegate.selected_index = 0;
} else {
delegate.selected_index =
@@ -278,10 +321,14 @@ impl PickerDelegate for BranchListDelegate {
})
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(branch) = self.matches.get(self.selected_index()) else {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index()) else {
return;
};
if entry.is_new {
self.create_branch(entry.branch.name.clone(), window, cx);
return;
}
let current_branch = self.repo.as_ref().map(|repo| {
repo.update(cx, |repo, _| {
@@ -291,14 +338,14 @@ impl PickerDelegate for BranchListDelegate {
if current_branch
.flatten()
.is_some_and(|current_branch| current_branch == branch.name())
.is_some_and(|current_branch| current_branch == entry.branch.name)
{
cx.emit(DismissEvent);
return;
}
cx.spawn_in(window, {
let branch = branch.clone();
let branch = entry.branch.clone();
|picker, mut cx| async move {
let branch_change_task = picker.update(&mut cx, |this, cx| {
let repo = this
@@ -311,22 +358,8 @@ impl PickerDelegate for BranchListDelegate {
let cx = cx.to_async();
anyhow::Ok(async move {
match branch {
BranchEntry::Branch(StringMatch {
string: branch_name,
..
})
| BranchEntry::History(branch_name) => {
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
.await?
}
BranchEntry::NewBranch { name: branch_name } => {
cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
.await??;
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
.await?
}
}
cx.update(|cx| repo.read(cx).change_branch(branch.name.to_string()))?
.await?
})
})??;
@@ -346,16 +379,35 @@ impl PickerDelegate for BranchListDelegate {
cx.emit(DismissEvent);
}
fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
None
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let shortened_branch_name =
util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
let entry = &self.matches[ix];
let (commit_time, subject) = entry
.branch
.most_recent_commit
.as_ref()
.map(|commit| {
let subject = commit.subject.clone();
let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
.unwrap_or_else(|_| OffsetDateTime::now_utc());
let formatted_time = format_local_timestamp(
commit_time,
OffsetDateTime::now_utc(),
time_format::TimestampFormat::Relative,
);
(Some(formatted_time), Some(subject))
})
.unwrap_or_else(|| (None, None));
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
@@ -366,29 +418,68 @@ impl PickerDelegate for BranchListDelegate {
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.when(matches!(hit, BranchEntry::History(_)), |el| {
el.end_slot(
Icon::new(IconName::HistoryRerun)
.color(Color::Muted)
.size(IconSize::Small),
)
})
.map(|el| match hit {
BranchEntry::Branch(branch) => {
let highlights: Vec<_> = branch
.positions
.iter()
.filter(|index| index < &&self.branch_name_trailoff_after)
.copied()
.collect();
el.child(HighlightedLabel::new(shortened_branch_name, highlights))
}
BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
BranchEntry::NewBranch { name } => {
el.child(Label::new(format!("Create branch '{name}'")))
}
}),
.child(
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.flex_shrink()
.overflow_x_hidden()
.gap_2()
.justify_between()
.child(div().flex_shrink().overflow_x_hidden().child(
if entry.is_new {
Label::new(format!(
"Create branch \"{}\"",
entry.branch.name
))
.single_line()
.into_any_element()
} else {
HighlightedLabel::new(
entry.branch.name.clone(),
entry.positions.clone(),
)
.truncate()
.into_any_element()
},
))
.when_some(commit_time, |el, commit_time| {
el.child(
Label::new(commit_time)
.size(LabelSize::Small)
.color(Color::Muted)
.into_element(),
)
}),
)
.when(self.style == BranchListStyle::Modal, |el| {
el.child(div().max_w_96().child({
let message = if entry.is_new {
if let Some(current_branch) =
self.repo.as_ref().and_then(|repo| {
repo.read(cx).current_branch().map(|b| b.name.clone())
})
{
format!("based off {}", current_branch)
} else {
"based off the current branch".to_string()
}
} else {
subject.unwrap_or("no commits found".into()).to_string()
};
Label::new(message)
.size(LabelSize::Small)
.truncate()
.color(Color::Muted)
}))
}),
),
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
None
}
}

View File

@@ -4,7 +4,7 @@ If you can accurately express the change in just the subject line, don't include
Don't repeat information from the subject line in the message body.
Only return the commit message in your response. Do not include any additional meta-commentary about the task.
Only return the commit message in your response. Do not include any additional meta-commentary about the task. Do not include the raw diff output in the commit message.
Follow good Git style:

View File

@@ -2,9 +2,9 @@
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use git::Commit;
use git::{Commit, GenerateCommitMessage};
use panel::{panel_button, panel_editor_style, panel_filled_button};
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
use ui::{prelude::*, KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip};
use editor::{Editor, EditorElement};
use gpui::*;
@@ -65,11 +65,11 @@ pub fn init(cx: &mut App) {
}
pub struct CommitModal {
branch_list: Entity<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
properties: ModalContainerProperties,
branch_list_handle: PopoverMenuHandle<BranchList>,
}
impl Focusable for CommitModal {
@@ -146,7 +146,6 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let active_repository = panel.active_repository.clone();
let suggested_commit_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
@@ -177,11 +176,7 @@ impl CommitModal {
let focus_handle = commit_editor.focus_handle(cx);
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
if !this
.branch_list
.focus_handle(cx)
.contains_focused(window, cx)
{
if !this.branch_list_handle.is_focused(window, cx) {
cx.emit(DismissEvent);
}
})
@@ -190,11 +185,11 @@ impl CommitModal {
let properties = ModalContainerProperties::new(window, 50);
Self {
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
git_panel,
commit_editor,
restore_dock,
properties,
branch_list_handle: PopoverMenuHandle::default(),
}
}
@@ -232,34 +227,29 @@ impl CommitModal {
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
let active_repo = git_panel.active_repository.clone();
(
branch,
can_commit,
tooltip,
title,
co_authors,
generate_commit_message,
active_repo,
)
});
let branch = active_repo
.as_ref()
.and_then(|repo| repo.read(cx).repository_entry.branch())
.map(|b| b.name.clone())
.unwrap_or_else(|| "<no branch>".into());
let branch_picker_button = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
@@ -276,10 +266,8 @@ impl CommitModal {
.style(ButtonStyle::Transparent);
let branch_picker = PopoverMenu::new("popover-button")
.menu({
let branch_list = self.branch_list.clone();
move |_window, _cx| Some(branch_list.clone())
})
.menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
.with_handle(self.branch_list_handle.clone())
.trigger_with_tooltip(
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
@@ -289,6 +277,7 @@ impl CommitModal {
x: px(0.0),
y: px(-2.0),
});
let focus_handle = self.focus_handle(cx);
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
@@ -300,12 +289,9 @@ impl CommitModal {
None
};
let panel_editor_focus_handle =
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
let commit_button = panel_filled_button(commit_label)
.tooltip({
let panel_editor_focus_handle = panel_editor_focus_handle.clone();
let panel_editor_focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
}
@@ -330,7 +316,14 @@ impl CommitModal {
.child(
h_flex()
.gap_1()
.child(branch_picker)
.flex_shrink()
.overflow_x_hidden()
.child(
h_flex()
.flex_shrink()
.overflow_x_hidden()
.child(branch_picker),
)
.children(generate_commit_message)
.children(co_authors),
)
@@ -357,6 +350,14 @@ impl CommitModal {
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
cx.emit(DismissEvent);
}
fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.branch_list_handle.is_focused(window, cx) {
self.focus_handle(cx).focus(window)
} else {
self.branch_list_handle.toggle(window, cx);
}
}
}
impl Render for CommitModal {
@@ -372,11 +373,24 @@ impl Render for CommitModal {
.key_context("GitCommit")
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
this.git_panel.update(cx, |panel, cx| {
panel.generate_commit_message(cx);
})
}))
.on_action(
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
this.branch_list.update(cx, |branch_list, cx| {
branch_list.popover_handle.toggle(window, cx);
})
this.toggle_branch_selector(window, cx);
}),
)
.on_action(
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
this.toggle_branch_selector(window, cx);
}),
)
.on_action(
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
this.toggle_branch_selector(window, cx);
}),
)
.elevation_3(cx)

File diff suppressed because it is too large Load Diff

View File

@@ -58,15 +58,22 @@ pub struct GitPanelSettingsContent {
///
/// Default: inherits editor scrollbar settings
pub scrollbar: Option<ScrollbarSettings>,
/// What the default branch name should be when
/// `init.defaultBranch` is not set in git
///
/// Default: main
pub fallback_branch_name: Option<String>,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct GitPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
pub status_style: StatusStyle,
pub scrollbar: ScrollbarSettings,
pub fallback_branch_name: String,
}
impl Settings for GitPanelSettings {

View File

@@ -1,9 +1,17 @@
use std::any::Any;
use ::settings::Settings;
use git::status::FileStatus;
use command_palette_hooks::CommandPaletteFilter;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use git_panel_settings::GitPanelSettings;
use gpui::App;
use gpui::{actions, App, Entity, FocusHandle};
use onboarding::{clear_dismissed, GitOnboardingModal};
use project::Project;
use project_diff::ProjectDiff;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
use ui::prelude::*;
use workspace::Workspace;
mod askpass_modal;
@@ -11,11 +19,14 @@ pub mod branch_picker;
mod commit_modal;
pub mod git_panel;
mod git_panel_settings;
pub mod onboarding;
pub mod picker_prompt;
pub mod project_diff;
mod remote_output_toast;
pub(crate) mod remote_output;
pub mod repository_selector;
actions!(git, [ResetOnboarding]);
pub fn init(cx: &mut App) {
GitPanelSettings::register(cx);
branch_picker::init(cx);
@@ -25,67 +36,516 @@ pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, cx| {
let project = workspace.project().read(cx);
if project.is_via_collab() {
if project.is_read_only(cx) {
return;
}
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
if !project.is_via_collab() {
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
});
});
}
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(window, cx);
panel.stage_all(action, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, window, cx);
panel.unstage_all(action, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&[
zed_actions::OpenGitIntegrationOnboarding.type_id(),
// ResetOnboarding.type_id(),
]);
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
workspace.register_action(
move |workspace, _: &zed_actions::OpenGitIntegrationOnboarding, window, cx| {
GitOnboardingModal::toggle(workspace, window, cx)
},
);
workspace.register_action(move |_, _: &ResetOnboarding, window, cx| {
clear_dismissed(cx);
window.refresh();
});
workspace.register_action(|workspace, _action: &git::Init, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
panel.git_init(window, cx);
});
});
})
.detach();
}
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
GitStatusIcon::new(status)
}
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
!project.read(cx).is_via_collab()
}
fn render_remote_button(
id: impl Into<SharedString>,
branch: &Branch,
keybinding_target: Option<FocusHandle>,
show_fetch_button: bool,
) -> Option<impl IntoElement> {
let id = id.into();
let upstream = branch.upstream.as_ref();
match upstream {
Some(Upstream {
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
..
}) => match (*ahead, *behind) {
(0, 0) if show_fetch_button => {
Some(remote_button::render_fetch_button(keybinding_target, id))
}
(0, 0) => None,
(ahead, 0) => Some(remote_button::render_push_button(
keybinding_target.clone(),
id,
ahead,
)),
(ahead, behind) => Some(remote_button::render_pull_button(
keybinding_target.clone(),
id,
ahead,
behind,
)),
},
Some(Upstream {
tracking: UpstreamTracking::Gone,
..
}) => Some(remote_button::render_republish_button(
keybinding_target,
id,
)),
None => Some(remote_button::render_publish_button(keybinding_target, id)),
}
}
mod remote_button {
use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle};
use ui::{
div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable,
ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize,
IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu,
RenderOnce, SharedString, Styled, Tooltip, Window,
};
pub fn render_fetch_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Fetch",
0,
0,
Some(IconName::ArrowCircle),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Fetch), cx);
},
move |window, cx| {
git_action_tooltip(
"Fetch updates from remote",
&git::Fetch,
"git fetch",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_push_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
ahead: u32,
) -> SplitButton {
SplitButton::new(
id,
"Push",
ahead as usize,
0,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Push committed changes to remote",
&git::Push,
"git push",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_pull_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
ahead: u32,
behind: u32,
) -> SplitButton {
SplitButton::new(
id,
"Pull",
ahead as usize,
behind as usize,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Pull), cx);
},
move |window, cx| {
git_action_tooltip(
"Pull",
&git::Pull,
"git pull",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_publish_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Publish",
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Publish branch to remote",
&git::Push,
"git push --set-upstream",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_republish_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Republish",
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Re-publish branch to remote",
&git::Push,
"git push --set-upstream",
keybinding_target.clone(),
window,
cx,
)
},
)
}
fn git_action_tooltip(
label: impl Into<SharedString>,
action: &dyn Action,
command: impl Into<SharedString>,
focus_handle: Option<FocusHandle>,
window: &mut Window,
cx: &mut App,
) -> AnyView {
let label = label.into();
let command = command.into();
if let Some(handle) = focus_handle {
Tooltip::with_meta_in(
label.clone(),
Some(action),
command.clone(),
&handle,
window,
cx,
)
} else {
Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
}
}
fn render_git_action_menu(
id: impl Into<ElementId>,
keybinding_target: Option<FocusHandle>,
) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("split-button-right")
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::None)
.child(
div()
.px_1()
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
),
)
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
el.context(keybinding_target.clone())
})
.action("Fetch", git::Fetch.boxed_clone())
.action("Pull", git::Pull.boxed_clone())
.separator()
.action("Push", git::Push.boxed_clone())
.action("Force Push", git::ForcePush.boxed_clone())
}))
})
.anchor(Corner::TopRight)
}
#[derive(IntoElement)]
pub struct SplitButton {
pub left: ButtonLike,
pub right: AnyElement,
}
impl SplitButton {
#[allow(clippy::too_many_arguments)]
fn new(
id: impl Into<SharedString>,
left_label: impl Into<SharedString>,
ahead_count: usize,
behind_count: usize,
left_icon: Option<IconName>,
keybinding_target: Option<FocusHandle>,
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
let id = id.into();
fn count(count: usize) -> impl IntoElement {
h_flex()
.ml_neg_px()
.h(rems(0.875))
.items_center()
.overflow_hidden()
.px_0p5()
.child(
Label::new(count.to_string())
.size(LabelSize::XSmall)
.line_height_style(LineHeightStyle::UiLabel),
)
}
let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
format!("split-button-left-{}", id).into(),
))
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.when(should_render_counts, |this| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.when(behind_count > 0, |this| {
this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
.child(count(behind_count))
})
.when(ahead_count > 0, |this| {
this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
.child(count(ahead_count))
}),
)
})
.when_some(left_icon, |this, left_icon| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.child(Icon::new(left_icon).size(IconSize::XSmall)),
)
})
.child(
div()
.child(Label::new(left_label).size(LabelSize::Small))
.mr_0p5(),
)
.on_click(left_on_click)
.tooltip(tooltip);
let right = render_git_action_menu(
ElementId::Name(format!("split-button-right-{}", id).into()),
keybinding_target,
)
.into_any_element();
Self { left, right }
}
}
impl RenderOnce for SplitButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.rounded_sm()
.border_1()
.border_color(cx.theme().colors().text_muted.alpha(0.12))
.child(div().flex_grow().child(self.left))
.child(
div()
.h_full()
.w_px()
.bg(cx.theme().colors().text_muted.alpha(0.16)),
)
.child(self.right)
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}])
}
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct GitStatusIcon {
status: FileStatus,
}
impl GitStatusIcon {
pub fn new(status: FileStatus) -> Self {
Self { status }
}
}
impl RenderOnce for GitStatusIcon {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
let status = self.status;
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
worktree_status: code,
})
}
let modified = tracked_file_status(StatusCode::Modified);
let added = tracked_file_status(StatusCode::Added);
let deleted = tracked_file_status(StatusCode::Deleted);
let conflict = UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into();
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
}
}

View File

@@ -0,0 +1,267 @@
use gpui::{
svg, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global,
MouseDownEvent, Render,
};
use ui::{prelude::*, ButtonLike, TintColor, Tooltip};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::git_panel::GitPanel;
macro_rules! git_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "Git Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "Git Onboarding", $($key $(= $value)?),+);
};
}
/// Introduces user to the Git Panel and overall improved Git support
pub struct GitOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl GitOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<GitPanel>(window, cx);
});
cx.emit(DismissEvent);
git_onboarding_event!("Open Panel Clicked");
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/git");
cx.notify();
git_onboarding_event!("Blog Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for GitOnboardingModal {}
impl Focusable for GitOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for GitOnboardingModal {}
impl Render for GitOnboardingModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_height = window.viewport_size().height;
let max_height = window_height - px(200.);
let base = v_flex()
.id("git-onboarding")
.key_context("GitOnboardingModal")
.relative()
.w(px(450.))
.h_full()
.max_h(max_height)
.p_4()
.gap_2()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
git_onboarding_event!("Cancelled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div().p_1p5().absolute().inset_0().h(px(160.)).child(
svg()
.path("icons/git_onboarding_bg.svg")
.text_color(cx.theme().colors().icon_disabled)
.w(px(420.))
.h(px(128.))
.overflow_hidden(),
),
)
.child(
v_flex()
.w_full()
.gap_1()
.child(
Label::new("Introducing")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Native Git Support").size(HeadlineSize::Large)),
)
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
git_onboarding_event!("Cancelled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
));
let open_panel_button = Button::new("open-panel", "Get Started with the Git Panel")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_blog));
let copy = "First-class support for staging, committing, pulling, pushing, viewing diffs, and more. All without leaving Zed.";
base.child(Label::new(copy).color(Color::Muted)).child(
v_flex()
.w_full()
.mt_2()
.gap_2()
.child(open_panel_button)
.child(blog_post_button),
)
}
}
/// Prompts the user to try Zed's git features
pub struct GitBanner {
dismissed: bool,
}
#[derive(Clone)]
struct GitBannerGlobal(Entity<GitBanner>);
impl Global for GitBannerGlobal {}
impl GitBanner {
pub fn new(cx: &mut Context<Self>) -> Self {
cx.set_global(GitBannerGlobal(cx.entity()));
Self {
dismissed: get_dismissed(),
}
}
fn should_show(&self, _cx: &mut App) -> bool {
!self.dismissed
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
git_onboarding_event!("Banner Dismissed");
persist_dismissed(cx);
self.dismissed = true;
cx.notify();
}
}
const DISMISSED_AT_KEY: &str = "zed_git_banner_dismissed_at";
fn get_dismissed() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_AT_KEY)
.log_err()
.map_or(false, |dismissed| dismissed.is_some())
}
fn persist_dismissed(cx: &mut App) {
cx.spawn(|_| {
let time = chrono::Utc::now().to_rfc3339();
db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
})
.detach_and_log_err(cx);
}
pub(crate) fn clear_dismissed(cx: &mut App) {
cx.defer(|cx| {
cx.global::<GitBannerGlobal>()
.clone()
.0
.update(cx, |this, cx| {
this.dismissed = false;
cx.notify();
});
});
cx.spawn(|_| db::kvp::KEY_VALUE_STORE.delete_kvp(DISMISSED_AT_KEY.into()))
.detach_and_log_err(cx);
}
impl Render for GitBanner {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.should_show(cx) {
return div();
}
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
let banner = h_flex()
.rounded_sm()
.border_1()
.border_color(border_color)
.child(
ButtonLike::new("try-git")
.child(
h_flex()
.h_full()
.items_center()
.gap_1()
.child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small))
.child(
h_flex()
.gap_0p5()
.child(
Label::new("Introducing:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Label::new("Git Support").size(LabelSize::Small)),
),
)
.on_click(cx.listener(|this, _, window, cx| {
git_onboarding_event!("Banner Clicked");
this.dismiss(cx);
window.dispatch_action(
Box::new(zed_actions::OpenGitIntegrationOnboarding),
cx,
)
})),
)
.child(
div().border_l_1().border_color(border_color).child(
IconButton::new("close", IconName::Close)
.icon_size(IconSize::Indicator)
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
.tooltip(|window, cx| {
Tooltip::with_meta(
"Close Announcement Banner",
None,
"It won't show again for this feature",
window,
cx,
)
}),
),
);
div().pr_2().child(banner)
}
}

View File

@@ -1,4 +1,3 @@
use anyhow::{anyhow, Result};
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -26,9 +25,9 @@ pub fn prompt(
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Option<usize>>> {
) -> Task<Option<usize>> {
if options.is_empty() {
return Task::ready(Err(anyhow!("No options")));
return Task::ready(None);
}
let prompt = prompt.to_string().into();
@@ -37,15 +36,17 @@ pub fn prompt(
let (tx, rx) = oneshot::channel();
let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
workspace.update_in(&mut cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
PickerPrompt::new(delegate, 34., window, cx)
workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
PickerPrompt::new(delegate, 34., window, cx)
})
})
})?;
.ok();
match rx.await {
Ok(selection) => Some(selection).transpose(),
Err(_) => anyhow::Ok(None), // User cancelled
Ok(selection) => Some(selection),
Err(_) => None, // User cancelled
}
})
}
@@ -94,14 +95,14 @@ pub struct PickerPromptDelegate {
all_options: Vec<SharedString>,
selected_index: usize,
max_match_length: usize,
tx: Option<oneshot::Sender<Result<usize>>>,
tx: Option<oneshot::Sender<usize>>,
}
impl PickerPromptDelegate {
pub fn new(
prompt: Arc<str>,
options: Vec<SharedString>,
tx: oneshot::Sender<Result<usize>>,
tx: oneshot::Sender<usize>,
max_chars: usize,
) -> Self {
Self {
@@ -200,7 +201,7 @@ impl PickerDelegate for PickerPromptDelegate {
return;
};
self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
self.tx.take().map(|tx| tx.send(option.candidate_id));
cx.emit(DismissEvent);
}

View File

@@ -1,4 +1,7 @@
use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
use crate::{
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
remote_button::{render_publish_button, render_push_button},
};
use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::HashSet;
@@ -7,10 +10,11 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent,
};
use feature_flags::FeatureFlagViewExt;
use futures::StreamExt;
use git::{
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
};
use gpui::{
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
@@ -24,27 +28,27 @@ use project::{
};
use std::any::{Any, TypeId};
use theme::ActiveTheme;
use ui::{prelude::*, vertical_divider, Tooltip};
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
use util::ResultExt as _;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
searchable::SearchableItemHandle,
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
};
actions!(git, [Diff]);
actions!(git, [Diff, Add]);
pub struct ProjectDiff {
project: Entity<Project>,
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
project: Entity<Project>,
git_store: Entity<GitStore>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
current_branch: Option<Branch>,
_task: Task<Result<()>>,
_subscription: Subscription,
}
@@ -63,13 +67,13 @@ const NEW_NAMESPACE: &'static str = "2";
impl ProjectDiff {
pub(crate) fn register(
_: &mut Workspace,
window: Option<&mut Window>,
workspace: &mut Workspace,
_window: Option<&mut Window>,
cx: &mut Context<Workspace>,
) {
let Some(window) = window else { return };
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
workspace.register_action(Self::deploy);
workspace.register_action(Self::deploy);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
});
workspace::register_serializable_item::<ProjectDiff>(cx);
@@ -121,6 +125,12 @@ impl ProjectDiff {
}
}
pub fn autoscroll(&self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx);
})
}
fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
@@ -138,6 +148,7 @@ impl ProjectDiff {
window,
cx,
);
diff_display_editor.disable_inline_diagnostics();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
@@ -166,7 +177,7 @@ impl ProjectDiff {
let this = cx.weak_entity();
|cx| Self::handle_status_updates(this, recv, cx)
});
// Kick off a refresh immediately
// Kick of a refresh immediately
*send.borrow_mut() = ();
Self {
@@ -178,6 +189,7 @@ impl ProjectDiff {
multibuffer,
pending_scroll: None,
update_needed: send,
current_branch: None,
_task: worker,
_subscription: git_store_subscription,
}
@@ -443,6 +455,20 @@ impl ProjectDiff {
mut cx: AsyncWindowContext,
) -> Result<()> {
while let Some(_) = recv.next().await {
this.update(&mut cx, |this, cx| {
let new_branch =
this.git_store
.read(cx)
.active_repository()
.and_then(|active_repository| {
active_repository.read(cx).current_branch().cloned()
});
if new_branch != this.current_branch {
this.current_branch = new_branch;
cx.notify();
}
})?;
let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
@@ -452,7 +478,10 @@ impl ProjectDiff {
})?;
}
}
this.update(&mut cx, |this, _| this.pending_scroll.take())?;
this.update(&mut cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
})?;
}
Ok(())
@@ -641,9 +670,11 @@ impl Item for ProjectDiff {
}
impl Render for ProjectDiff {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.multibuffer.read(cx).is_empty();
let can_push_and_pull = crate::can_push_and_pull(&self.project, cx);
div()
.track_focus(&self.focus_handle)
.key_context(if is_empty { "EmptyPane" } else { "GitDiff" })
@@ -653,7 +684,61 @@ impl Render for ProjectDiff {
.justify_center()
.size_full()
.when(is_empty, |el| {
el.child(Label::new("No uncommitted changes"))
el.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_around()
.child(Label::new("No uncommitted changes")),
)
.when(can_push_and_pull, |this_div| {
let keybinding_focus_handle = self.focus_handle(cx);
this_div.when_some(self.current_branch.as_ref(), |this_div, branch| {
let remote_button = crate::render_remote_button(
"project-diff-remote-button",
branch,
Some(keybinding_focus_handle.clone()),
false,
);
match remote_button {
Some(button) => {
this_div.child(h_flex().justify_around().child(button))
}
None => this_div.child(
h_flex()
.justify_around()
.child(Label::new("Remote up to date")),
),
}
})
})
.map(|this| {
let keybinding_focus_handle = self.focus_handle(cx).clone();
this.child(
h_flex().justify_around().mt_1().child(
Button::new("project-diff-close-button", "Close")
// .style(ButtonStyle::Transparent)
.key_binding(KeyBinding::for_action_in(
&CloseActiveItem::default(),
&keybinding_focus_handle,
window,
cx,
))
.on_click(move |_, window, cx| {
window.focus(&keybinding_focus_handle);
window.dispatch_action(
Box::new(CloseActiveItem::default()),
cx,
);
}),
),
)
}),
)
})
.when(!is_empty, |el| el.child(self.editor.clone()))
}
@@ -730,23 +815,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();
}
}
@@ -900,7 +992,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)
})),
)
},
@@ -920,7 +1012,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)
})),
),
)
@@ -941,6 +1033,287 @@ impl Render for ProjectDiffToolbar {
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct ProjectDiffEmptyState {
pub no_repo: bool,
pub can_push_and_pull: bool,
pub focus_handle: Option<FocusHandle>,
pub current_branch: Option<Branch>,
// has_pending_commits: bool,
// ahead_of_remote: bool,
// no_git_repository: bool,
}
impl RenderOnce for ProjectDiffEmptyState {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool {
match self.current_branch {
Some(Branch {
upstream:
Some(Upstream {
tracking:
UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead, behind, ..
}),
..
}),
..
}) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true,
_ => false,
}
};
let change_count = |current_branch: &Branch| -> (usize, usize) {
match current_branch {
Branch {
upstream:
Some(Upstream {
tracking:
UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead, behind, ..
}),
..
}),
..
} => (*ahead as usize, *behind as usize),
_ => (0, 0),
}
};
let not_ahead_or_behind = status_against_remote(0, 0);
let ahead_of_remote = status_against_remote(1, 0);
let branch_not_on_remote = if let Some(branch) = self.current_branch.as_ref() {
branch.upstream.is_none()
} else {
false
};
let has_branch_container = |branch: &Branch| {
h_flex()
.max_w(px(420.))
.bg(cx.theme().colors().text.opacity(0.05))
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.gap_8()
.px_6()
.py_4()
.map(|this| {
if ahead_of_remote {
let ahead_count = change_count(branch).0;
let ahead_string = format!("{} Commits Ahead", ahead_count);
this.child(
v_flex()
.child(Headline::new(ahead_string).size(HeadlineSize::Small))
.child(
Label::new(format!("Push your changes to {}", branch.name))
.color(Color::Muted),
),
)
.child(div().child(render_push_button(
self.focus_handle,
"push".into(),
ahead_count as u32,
)))
} else if branch_not_on_remote {
this.child(
v_flex()
.child(Headline::new("Publish Branch").size(HeadlineSize::Small))
.child(
Label::new(format!("Create {} on remote", branch.name))
.color(Color::Muted),
),
)
.child(
div().child(render_publish_button(self.focus_handle, "publish".into())),
)
} else {
this.child(Label::new("Remote status unknown").color(Color::Muted))
}
})
};
v_flex().size_full().items_center().justify_center().child(
v_flex()
.gap_1()
.when(self.no_repo, |this| {
// TODO: add git init
this.text_center()
.child(Label::new("No Repository").color(Color::Muted))
})
.map(|this| {
if not_ahead_or_behind && self.current_branch.is_some() {
this.text_center()
.child(Label::new("No Changes").color(Color::Muted))
} else {
this.when_some(self.current_branch.as_ref(), |this, branch| {
this.child(has_branch_container(&branch))
})
}
}),
)
}
}
// .when(self.can_push_and_pull, |this| {
// let remote_button = crate::render_remote_button(
// "project-diff-remote-button",
// &branch,
// self.focus_handle.clone(),
// false,
// );
// match remote_button {
// Some(button) => {
// this.child(h_flex().justify_around().child(button))
// }
// None => this.child(
// h_flex()
// .justify_around()
// .child(Label::new("Remote up to date")),
// ),
// }
// }),
//
// // .map(|this| {
// this.child(h_flex().justify_around().mt_1().child(
// Button::new("project-diff-close-button", "Close").when_some(
// self.focus_handle.clone(),
// |this, focus_handle| {
// this.key_binding(KeyBinding::for_action_in(
// &CloseActiveItem::default(),
// &focus_handle,
// window,
// cx,
// ))
// .on_click(move |_, window, cx| {
// window.focus(&focus_handle);
// window
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
// })
// },
// ),
// ))
// }),
mod preview {
use git::repository::{
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
};
use ui::prelude::*;
use super::ProjectDiffEmptyState;
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for ProjectDiffEmptyState {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
let unknown_upstream: Option<UpstreamTracking> = None;
let ahead_of_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
ahead: 2,
behind: 0,
}
.into(),
);
let not_ahead_or_behind_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
ahead: 0,
behind: 0,
}
.into(),
);
fn branch(upstream: Option<UpstreamTracking>) -> Branch {
Branch {
is_head: true,
name: "some-branch".into(),
upstream: upstream.map(|tracking| Upstream {
ref_name: "origin/some-branch".into(),
tracking,
}),
most_recent_commit: Some(CommitSummary {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
has_parent: true,
}),
}
}
let no_repo_state = ProjectDiffEmptyState {
no_repo: true,
can_push_and_pull: false,
focus_handle: None,
current_branch: None,
};
let no_changes_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(not_ahead_or_behind_upstream)),
};
let ahead_of_upstream_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(ahead_of_upstream)),
};
let unknown_upstream_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(unknown_upstream)),
};
let (width, height) = (px(480.), px(320.));
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
])
.vertical()])
.into_any_element()
}
}
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod tests {

View File

@@ -0,0 +1,152 @@
use anyhow::Context as _;
use git::repository::{Remote, RemoteCommandOutput};
use linkify::{LinkFinder, LinkKind};
use ui::SharedString;
use util::ResultExt as _;
#[derive(Clone)]
pub enum RemoteAction {
Fetch,
Pull(Remote),
Push(SharedString, Remote),
}
impl RemoteAction {
pub fn name(&self) -> &'static str {
match self {
RemoteAction::Fetch => "fetch",
RemoteAction::Pull(_) => "pull",
RemoteAction::Push(_, _) => "push",
}
}
}
pub enum SuccessStyle {
Toast,
ToastWithLog { output: RemoteCommandOutput },
PushPrLink { link: String },
}
pub struct SuccessMessage {
pub message: String,
pub style: SuccessStyle,
}
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
match action {
RemoteAction::Fetch => {
if output.stderr.is_empty() {
SuccessMessage {
message: "Already up to date".into(),
style: SuccessStyle::Toast,
}
} else {
SuccessMessage {
message: "Synchronized with remotes".into(),
style: SuccessStyle::ToastWithLog { output },
}
}
}
RemoteAction::Pull(remote_ref) => {
let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result<u32> {
let last_line = output
.stdout
.lines()
.last()
.context("Failed to get last line of output")?
.trim();
let files_changed = last_line
.split_whitespace()
.next()
.context("Failed to get first word of last line")?
.parse()?;
Ok(files_changed)
};
if output.stderr.starts_with("Everything up to date") {
SuccessMessage {
message: output.stderr.trim().to_owned(),
style: SuccessStyle::Toast,
}
} else if output.stdout.starts_with("Updating") {
let files_changed = get_changes(&output).log_err();
let message = if let Some(files_changed) = files_changed {
format!(
"Received {} file change{} from {}",
files_changed,
if files_changed == 1 { "" } else { "s" },
remote_ref.name
)
} else {
format!("Fast forwarded from {}", remote_ref.name)
};
SuccessMessage {
message,
style: SuccessStyle::ToastWithLog { output },
}
} else if output.stdout.starts_with("Merge") {
let files_changed = get_changes(&output).log_err();
let message = if let Some(files_changed) = files_changed {
format!(
"Merged {} file change{} from {}",
files_changed,
if files_changed == 1 { "" } else { "s" },
remote_ref.name
)
} else {
format!("Merged from {}", remote_ref.name)
};
SuccessMessage {
message,
style: SuccessStyle::ToastWithLog { output },
}
} else if output.stdout.contains("Successfully rebased") {
SuccessMessage {
message: format!("Successfully rebased from {}", remote_ref.name),
style: SuccessStyle::ToastWithLog { output },
}
} else {
SuccessMessage {
message: format!("Successfully pulled from {}", remote_ref.name),
style: SuccessStyle::ToastWithLog { output },
}
}
}
RemoteAction::Push(branch_name, remote_ref) => {
if output.stderr.contains("* [new branch]") {
let style = if output.stderr.contains("Create a pull request") {
let finder = LinkFinder::new();
let first_link = finder
.links(&output.stderr)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.next();
if let Some(link) = first_link {
let link = output.stderr[link].to_string();
SuccessStyle::PushPrLink { link }
} else {
SuccessStyle::ToastWithLog { output }
}
} else {
SuccessStyle::ToastWithLog { output }
};
SuccessMessage {
message: format!("Published {} to {}", branch_name, remote_ref.name),
style,
}
} else if output.stderr.starts_with("Everything up to date") {
SuccessMessage {
message: output.stderr.trim().to_owned(),
style: SuccessStyle::Toast,
}
} else {
SuccessMessage {
message: format!("Pushed {} to {}", branch_name, remote_ref.name),
style: SuccessStyle::ToastWithLog { output },
}
}
}
}
}

View File

@@ -1,227 +0,0 @@
use std::{ops::Range, time::Duration};
use git::repository::{Remote, RemoteCommandOutput};
use gpui::{
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
StyledText, Task, UnderlineStyle, WeakEntity,
};
use itertools::Itertools;
use linkify::{LinkFinder, LinkKind};
use ui::{
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
Render, SharedString, Styled, StyledExt, Window,
};
use workspace::{
notifications::{Notification, NotificationId},
Workspace,
};
pub enum RemoteAction {
Fetch,
Pull,
Push(Remote),
}
struct InfoFromRemote {
name: SharedString,
remote_text: SharedString,
links: Vec<Range<usize>>,
}
pub struct RemoteOutputToast {
_workspace: WeakEntity<Workspace>,
_id: NotificationId,
message: SharedString,
remote_info: Option<InfoFromRemote>,
_dismiss_task: Task<()>,
focus_handle: FocusHandle,
}
impl Focusable for RemoteOutputToast {
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Notification for RemoteOutputToast {}
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
impl RemoteOutputToast {
pub fn new(
action: RemoteAction,
output: RemoteCommandOutput,
id: NotificationId,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let task = cx.spawn({
let workspace = workspace.clone();
let id = id.clone();
|_, mut cx| async move {
cx.background_executor()
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
.await;
workspace
.update(&mut cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx);
})
.ok();
}
});
let mut message: SharedString;
let remote;
match action {
RemoteAction::Fetch | RemoteAction::Pull => {
if output.is_empty() {
message = "Up to date".into();
} else {
message = output.stderr.into();
}
remote = None;
}
RemoteAction::Push(remote_ref) => {
message = output.stdout.trim().to_string().into();
if message.is_empty() {
message = output.stderr.trim().to_string().into();
if message.is_empty() {
message = "Push Successful".into();
}
remote = None;
} else {
let remote_message = get_remote_lines(&output.stderr);
remote = if remote_message.is_empty() {
None
} else {
let finder = LinkFinder::new();
let links = finder
.links(&remote_message)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.collect_vec();
Some(InfoFromRemote {
name: remote_ref.name,
remote_text: remote_message.into(),
links,
})
}
}
}
}
Self {
_workspace: workspace,
_id: id,
message,
remote_info: remote,
_dismiss_task: task,
focus_handle: cx.focus_handle(),
}
}
}
impl Render for RemoteOutputToast {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
div()
.occlude()
.w_full()
.max_h(vh(0.8, window))
.elevation_3(cx)
.child(
v_flex()
.p_3()
.overflow_hidden()
.child(
h_flex()
.justify_between()
.items_start()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::GitBranch).color(Color::Default))
.child(Label::new("Git")),
)
.child(h_flex().child(
IconButton::new("close", IconName::Close).on_click(
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
),
)),
)
.child(Label::new(self.message.clone()).size(LabelSize::Default))
.when_some(self.remote_info.as_ref(), |this, remote_info| {
this.child(
div()
.border_1()
.border_color(Color::Muted.color(cx))
.rounded_lg()
.text_sm()
.mt_1()
.p_1()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::Cloud).color(Color::Default))
.child(
Label::new(remote_info.name.clone())
.size(LabelSize::Default),
),
)
.map(|div| {
let styled_text =
StyledText::new(remote_info.remote_text.clone())
.with_highlights(remote_info.links.iter().map(
|link| {
(
link.clone(),
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.0),
..Default::default()
}),
..Default::default()
},
)
},
));
let this = cx.weak_entity();
let text = InteractiveText::new("remote-message", styled_text)
.on_click(
remote_info.links.clone(),
move |ix, _window, cx| {
this.update(cx, |this, cx| {
if let Some(remote_info) = &this.remote_info {
cx.open_url(
&remote_info.remote_text
[remote_info.links[ix].clone()],
)
}
})
.ok();
},
);
div.child(text)
}),
)
}),
)
}
}
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
fn get_remote_lines(output: &str) -> String {
output
.lines()
.filter_map(|line| line.strip_prefix("remote:"))
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}

View File

@@ -402,7 +402,7 @@ impl Render for DataTable {
.overflow_hidden()
.border_1()
.border_color(rgb(0xE0E0E0))
.rounded_sm()
.rounded_md()
.child(
div()
.flex()

View File

@@ -69,7 +69,7 @@ struct ImageLoadingExample {}
impl ImageLoadingExample {
fn loading_element() -> impl IntoElement {
div().size_full().flex_none().p_0p5().rounded_xs().child(
div().size_full().flex_none().p_0p5().rounded_sm().child(
div().size_full().with_animation(
"loading-bg",
Animation::new(Duration::from_secs(3))
@@ -89,7 +89,7 @@ impl ImageLoadingExample {
.flex()
.items_center()
.justify_center()
.rounded_xs()
.rounded_sm()
.text_sm()
.text_color(fallback_color)
.border_1()

View File

@@ -140,7 +140,7 @@ impl TextInput {
));
}
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
fn cut(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
(&self.content[self.selected_range.clone()]).to_string(),

View File

@@ -16,7 +16,7 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
.active(|this| this.opacity(0.85))
.border_1()
.border_color(rgb(0xe0e0e0))
.rounded_sm()
.rounded_md()
.cursor_pointer()
.child(text.to_string())
.on_click(move |_, window, cx| on_click(window, cx))

Some files were not shown because too many files have changed in this diff Show More