Compare commits

..

122 Commits

Author SHA1 Message Date
Richard Feldman
d6a8b4cfb1 Give Tools control over how they render input/output 2025-03-05 23:32:51 -05:00
Richard Feldman
264ac61210 Initial pass at Lua syntax highlighting
Co-Authored-By: Danilo <danilo@zed.dev>
2025-03-05 20:35:51 -05:00
Michael Sloan
ad4742a5b8 Use a global for scripting tool in-memory fs
Suggested in https://github.com/zed-industries/zed/pull/26132#discussion_r1981670385
2025-03-05 17:30:07 -07:00
Richard Feldman
b0d4abb82e Persist in-memory filesystem between tool uses 2025-03-05 10:30:20 -05:00
smit
387ee46c46 project: Fix issue where Cmd+Click on an import opens the wrong file (#26120)
Closes #21974

`resolve_path_in_worktrees` function looks for provided path in each
worktree until valid file is found.

In this PR we priortize current buffer worktree before other worktrees,
because of edge case where, file with same name might exists in other
worktrees.

Updated tests to handle this case.

Release Notes:

- Fixed an issue where the wrong file from a different worktree would
open when using `Cmd + Click` on a file import.
2025-03-05 16:49:39 +05:30
Maksim Bondarenkov
89d89b8b2d docs: Update MSYS2 section to add information about CLI (#25882)
MSYS2 now provides CLI along with editor in Zed package:
https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-zed

Closes #ISSUE

Release Notes:

- N/A
2025-03-05 13:08:54 +08:00
AidanV
f07ae541ad vim: Add registers view (#25945)
Closes #18157

Release Notes:

- vim: Added `:reg[isters]` to show the current values of registers

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-04 21:59:19 -07:00
brian tan
ff0bb1f389 vim: Fix insert before in visual modes (#25603)
Closes #22536

Changes:
- Visual and visual block: Cursor at start of selection.
- Visual line: Cursor at start on line.
- Uses different handling since the selection does not actually change
in vline.

Release Notes:

- vim: Fixed insert before (`shift-i`) in visual modes.
2025-03-04 21:58:01 -07:00
0x2CA
9c7eee24bc vim: Fix ignoring cursor_shape settings (#25439)
Closes #ISSUE

[Block cursor in insert mode
#25322](https://github.com/zed-industries/zed/discussions/25322)

Respect the `cursor_shape` setting in insert mode

Release Notes:

- Fixed vim ignoring `cursor_shape` settings
2025-03-04 21:48:43 -07:00
Conrad Irwin
ec4719146a Fix . repeat for remapping surrounds/exchange actions (#26101)
Closes #ISSUE

cc @thomasheartman

Release Notes:

- vim: Fixes `.` repeat for remapped surrounds/exchange actions
2025-03-04 21:47:12 -07:00
0x2CA
47f8f891c8 vim: Fix "seed_search_query_from_cursor" : "selection" (#26107)
Closes #9311
Closes #14843

Release Notes:

- Fixed vim `"seed_search_query_from_cursor" : "selection"`
2025-03-04 21:46:58 -07:00
maan2003
d9d3b8847b nix: Bump flake to get Rust 1.85 (#26076)
old nixpkgs versions didn't have rust 1.85 and nix develop failed (1.85
is specified in rust-toolchain.toml).

ran `nix flake update` to bump the flake dependencies. it now works

Release Notes:

- N/A
2025-03-04 19:18:40 -08:00
Conrad Irwin
d7b90f4204 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>
2025-03-04 20:07:19 -07:00
brian tan
3e64f38ba0 vim: Add support for toggling boolean values (#25997)
Closes #10400
Closes https://github.com/zed-industries/zed/issues/17947

Changes:
- Let vim::increment find boolean values in the line and toggle them. 

Release Notes:

- vim: Added support for toggling boolean values with `ctrl-a`/`ctrl-x`
2025-03-05 03:00:44 +00:00
Thomas Heartman
82338e2c47 vim: Fix clear exchange not working (#25804)
Fixes two issues with the Vim exchange implementation:

1. The clear exchange implementation **didn't** clear the exchange. This
was due to us asking the editor to clear normal highlights instead of
background highlights.
2. Calling clear exchange also wouldn't cause the operator to be
cleared, so you would be left in operator = "cx".

I've added tests for both of these cases.

Partially closes #25750. It doesn't address the problem with dot repeat
not working for my custom bindings, but I don't know what would cause
that. I'd love to hear some thoughts on why that is. That might be a
problem on my part or it might be something with the code. Input would
be appreciated.

Release Notes:

- Fixed: Vim exchange's "clear exchange" function didn't clear the
exchange and kept you in operator pending mode.
2025-03-04 19:34:52 -07:00
Nico Lehmann
229e853874 Make buffer search aware of search direction (#24974)
This solves a couple of issues with Vim search by making the search
buffer and `SearchableItem` aware of the direction of the search. If
`SearchOptions::BACKWARDS` is set, all operations will be reversed. By
making `SearchableItem` aware of the direction, the correct active match
can be selected when searching backward.

Fixes #22506. This PR does not fix the last problem in that issue, but
that one is also tracked in #8049.

Release Notes:

- Fixes incorrect behavior of backward search in Vim mode
2025-03-04 19:27:37 -07:00
Ben Kunkle
ed13e05855 project search: Fix text cutoff in options help text (#26098)
Closes #25495

Release Notes:

- N/A
2025-03-05 01:15:11 +00:00
Conrad Irwin
674fb7621f 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>
2025-03-05 00:50:26 +00:00
Max Brunsfeld
fe18c73a07 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:25:58 -08:00
Marshall Bowers
befacfe8c9 assistant2: Prevent concurrent thread saving tasks (#26089)
This PR makes it so only one thread-saving task will be in flight at a
time.

Release Notes:

- N/A
2025-03-05 00:18:13 +00:00
Danilo Leal
54f0a729c2 assistant2: Adjust edit message actions (#26081)
Fine-tuning the visuals (namely, reducing font and keybinding size) and
passing `on_click` handlers to the Cancel & Regenerate actions.

Release Notes:

- N/A
2025-03-04 20:48:29 -03:00
Marshall Bowers
67f9b2b87f markdown: Only change the copy code icon to a check temporarily (#26079)
This PR makes it so the copy code icon only changes to a check
temporarily.

It will now revert to the "copy" icon after 2 seconds.


https://github.com/user-attachments/assets/e8983268-9710-4519-97a0-b28dc237b109

Release Notes:

- N/A
2025-03-04 23:02:43 +00:00
Richard Feldman
a4ec0af681 Add initial scripting_tool (#26066)
Just a basic implementation so we can start trying it out.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
2025-03-04 17:59:19 -05:00
Marshall Bowers
886d8c1cab markdown: Ensure code block copy button stays in the right spot (#26074)
This PR makes it so the copy button on Markdown code blocks stays
absolutely positioned even when scrolled:

<img width="1297" alt="Screenshot 2025-03-04 at 5 28 48 PM"
src="https://github.com/user-attachments/assets/b0d0fae9-ccd6-43c1-bef3-44d8d3c3e669"
/>

We achieve this by inserting a new parent element around both the copy
button and the code block itself so we can position the copy button
absolutely within that element.

Release Notes:

- N/A
2025-03-04 22:44:29 +00:00
Mikayla Maki
ebc5c213a2 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.
2025-03-04 21:58:26 +00:00
Joseph T. Lyons
0a2d938ac5 Do not include recent issues in issue response script (#26064)
Do not report issues that were created yesterday or today.

Release Notes:

- N/A
2025-03-04 16:13:53 -05:00
Kirill Bulatov
fc01f496a9 Fix font sizes not reacting on settings change (#26060)
Proper version of https://github.com/zed-industries/zed/pull/25425
When https://github.com/zed-industries/zed/pull/24857 returned font
updates on settings changes, settings values, not in-memory ones should
be compared.

This PR returns back the logic finally, and changes it to explicitly
track the settings values, not the in-memory ones.
Also adds the same tracking for UI font changes, which had never been
tracked before.

Release Notes:

- Fixed font sizes not reacting on settings change
2025-03-04 20:57:37 +00:00
Isac Ljung
db28b9bbde Add typescript-language-server and vtsls to list of available language servers (#26046)
Add the typescript language severs as lsp adapters.
This would allow language extensions to use them.
For example using on vue files to be able to run the vue-language-server
in
[hybridMode](https://github.com/vuejs/language-tools?tab=readme-ov-file#hybrid-mode-configuration-requires-vuelanguage-server-version-200).

Release Notes:

- Added `vtsls` and `typescript-language-server` to the list of
available language servers.
2025-03-04 15:49:27 -05:00
Cole Miller
0453cb2b06 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
2025-03-04 12:37:11 -05:00
Conrad Irwin
85211889e5 git: Fix project diff shortcuts (#26045)
Release Notes:

- git: Fix keyboard shortcut display in project diff view
2025-03-04 10:32:20 -07:00
Marshall Bowers
ad94642e83 markdown: Fix code block wrapping when horizontal scrolling is disabled (#26048)
This PR fixes an issue where code block wrapping was broken when not
using horizontal scrolling after
https://github.com/zed-industries/zed/pull/25956.

Release Notes:

- N/A
2025-03-04 12:24:08 -05:00
Bennet Bo Fenner
f4899d92a4 assistant2: Add support for editing the last message sent by the user (#26037)
https://github.com/user-attachments/assets/df46632b-dfeb-4991-ab2e-86829b72be9b

Closes #ISSUE

Release Notes:

- N/A
2025-03-04 17:57:42 +01:00
0x2CA
6685d85f49 vim: Fix increment step error (#26023)
Closes #12887

Release Notes:

- Fixed `x g ctrl-a` step
2025-03-04 09:53:35 -07:00
Felix Packard
161f8a1dd2 Fix "Open a file or project to get started" placeholder text not always shown (#26044)
Check that there are no `visible_worktrees` rather than checking
`worktrees` when deciding whether to display the "Open a file or project
to get started" text

Closes #25395

Release Notes:

- Fixed the "Open a file or project to get started" message not always
showing after all buffers have been closed
2025-03-04 09:21:16 -07:00
Nate Butler
6cdd7b7390 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:10:39 -05:00
Alex Ozer
0ec15d6b02 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-04 15:55:27 +00:00
Bennet Bo Fenner
909de2ca6f assistant2: Use cmd-n to create a new prompt editor when already in a prompt editor (#25935)
This flips the keybindings that are used to create a new thread/prompt
editor (only when you're already in a prompt editor)

Release Notes:

- N/A
2025-03-04 16:44:32 +01:00
Agus Zubiaga
f31749c81b edit predictions: Improve UX when there's no keybinding for accepting predictions (#25815)
If the user already binds `tab`/`alt-tab`/`alt-l` to a different action
in a conflicting context and hasn't assigned a different keybinding for
`editor::AcceptEditPrediction`, we would show broken popovers with no
bindings:

![CleanShot 2025-02-28 at 12 46
13@2x](https://github.com/user-attachments/assets/a2c6a8ad-5e11-46ef-8031-62e1e6900244)

Instead, they will now see an error-variant of every popover which
includes a tooltip with a short description and buttons to open the
keymap, and open a new docs section explaining the issue in detail and
how to fix it.

![CleanShot 2025-02-28 at 12 48
11@2x](https://github.com/user-attachments/assets/36329b1f-6374-4735-9fbc-8fccab70e881)

Note: I included the docs change in this PR because it's ok to deploy
before the release, as it also applies to existing versions.

Release Notes:

- edit predictions: Improve UX when there's no keybinding for accepting
predictions

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Danilo <danilo@zed.dev>
2025-03-04 11:28:36 -03:00
Joseph T. Lyons
76a81607de 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 13:50:53 +00:00
smit
7d22059a2f migration: Add for editor::GoToHunk and editor::GoToPrevHunk actions (#26017)
We modified few actions in
https://github.com/zed-industries/zed/pull/25846, which are:

`"editor::GoToHunk" -> ["editor::GoToHunk", { "center_cursor": true }]`
`"editor::GoToPrevHunk" -> ["editor::GoToPrevHunk", { "center_cursor":
true }]`

Also, recently we changed and added migration for:

`["editor::GoToPrevHunk", { "center_cursor": true }] ->
["editor::GoToPreviousHunk", { "center_cursor": true }] `

This means:

1. User that might still have `editor::GoToHunk` won't be automatically
migrated to `["editor::GoToHunk", { "center_cursor": true }]`. Note
value of `center_cursor` is false, in first case (default), and true in
second case.

2. User that might still have `editor::GoToPrevHunk` won't be
automatically migrated to `["editor::GoToPreviousHunk", {
"center_cursor": true }]`. Note, `editor::GoToPrevHunk` is renamed
since, it is now invalid action.

This PR adds those migrations.

cc: @marcospb19 

Release Notes:

- N/A
2025-03-04 18:39:29 +05:30
feeiyu
6b16a5555e Fix lost focus when navigating back in project search result (#22483)
Closes #22447

When navigate forward/back, the focus moves from the ProjectSearchView's
result editor to the Pane, and then move to the ProjectSearchView, but
the event `on_focus_in` not triggered for ProjectSearchView, causing the
result editor to lose focus eventually.


f6dabadaf7/crates/workspace/src/workspace.rs (L1372)


f6dabadaf7/crates/workspace/src/workspace.rs (L1385)

Considering that the navigation might be triggered again in the next
frame, so use `on_next_frame` in `on_focus` event to move focus to
result editor.

Next frame:
- the blur event triggered for result editor.
- focus move from ProjectSearchView to result editor in `on_focus` event
for ProjectSearchView
- navigate again, focus moves from result editor to Pane then move back
to ProjectSearchView
- the focus not change during this frame, so no focus event happened for
ProjectSearchView.

![fix lost
focus1229](https://github.com/user-attachments/assets/bfaac839-7bcf-40e7-b3b4-1423d0510594)

Release Notes:

- Fix lost focus when navigate back in project search result

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-04 13:06:44 +00:00
Kirill Bulatov
7ba2b258de 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
2025-03-04 14:47:27 +02:00
Boris Vassilev
cbb535f5eb Fix completion details on new clangd versions (#25405)
Fixes #16057

In newer versions of clangd, the switch labelDetailsSupport in the json
passed to the language server modifies the format of the returned json.
Zed handles well the old format, but misses the function parameters in
the new one. For example:
The old format looks like this:
```json
...
"label": " Window(int width, int height, const char *name, bool vsync, bool resizable)",
...
```
and with labelDetailsSupport = true:
```json
...
 "label": " Window",
 "labelDetails": {
     "detail": "(int width, int height, const char *name, bool vsync, bool resizable)"
 },
...
```
A simple solution is to just to not tell the language server that label
details are supported and force it to use the old format. This is a
dirty fix, but makes the completions behave like in the old versions of
clangd.

I do not know if this will break another language server. From what I've
found out most lsp-s do not depend on that setting and provide all
completion data either way. If not, this switch will need to be exposed
in a config or be at least lsp-dependant.

Lastly, I do not know Rust, maybe will need help to make a better fix
for the issue.

Release Notes:

- Fixed broken C++ completion suggestions
2025-03-04 14:30:03 +02:00
Finn Evers
20fc753f2b editor: Ensure correct tab icon is shown for files outside of the current project (#25933)
Closes #25885 

This PR improves the matching for file icons to tabs. 
Previously, the tab icon would be resolved based upon the relative path
in the current project. However, this caused the default file icon being
assigned to all files outside of the project, as the relative path for
these files would be empty.

Instead, `path_for_buffer` is now used which always returns a proper
file name even for paths outside the current project (as also stated [in
this
comment](fee9c67707/crates/editor/src/items.rs (L1689))).
As the file name is sufficient for matching icons to files, this fixes
the linked issue whilst not changing anything for previously properly
matched icons.

| `main` | This PR |
| --- | --- | 
| <img width="296" alt="main"
src="https://github.com/user-attachments/assets/e72b8b5d-aa1c-4a8e-903f-14239f5b8764"
/> | <img width="296" alt="PR"
src="https://github.com/user-attachments/assets/a736974a-ce41-4861-be3f-95448cc7ffd0"
/> |

Release Notes:

- Fixed wrong file icons being shown for files outside of the current
project.
2025-03-04 13:57:26 +02:00
Finn Evers
042fc82e99 python: Fix and improve highlighting (#25813)
Closes #25803
Closes #25707 

This PR fixes the highlighting regression described in #25803 by fixing
the priorities of highlights as described in the [comment within the
file
itself](5b66ea1563/crates/languages/src/python/highlights.scm (L1)).
A nice side-effect of this is that scoped constants or other identifiers
are now also more accurately highlighted, as seen in the screenshots
below.

While I was at it, I also adressed the highlighting issue for default
typed idenfiers.

| This PR | <img width="575" alt="PR"
src="https://github.com/user-attachments/assets/aed5cdd0-c31a-4794-8128-376944fddd2d"
/> |
| --- | --- |
| Preview | <img width="575" alt="preview"
src="https://github.com/user-attachments/assets/ae3fad35-d436-472c-aff0-16508304ccf7"
/> |
| Stable | <img width="575" alt="stable"
src="https://github.com/user-attachments/assets/3836427c-f1cc-42ea-b1a7-8f5bbbadf210"
/> |

Release Notes:

- Fixed constants not being highlighted in Python-files.
- Improved Python-highlighting for default function arguments and scoped
identifiers.
2025-03-04 08:41:10 +01:00
Finn Evers
27781a8a60 html: Add injections for style attributes and event handler attributes (#23659)
Closes #23653 

Before:
<img width="921" alt="before"
src="https://github.com/user-attachments/assets/e993df15-77a7-4b5a-b6fb-3415047914c0"
/>

After:
<img width="922" alt="after"
src="https://github.com/user-attachments/assets/1b4bd695-2985-46e2-8b55-576d32af0583"
/>

Release Notes:

- N/A
2025-03-04 09:12:25 +02:00
Joseph T. Lyons
33af6bce55 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:01:52 -05:00
Max Brunsfeld
563baf682e 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
2025-03-03 22:18:27 -08:00
张小白
11b79d0ab9 workspace: Add trailing / to directories on completion when using OpenPathPrompt (#25430)
Closes #25045

With the setting `"use_system_path_prompts": false`, previously, if the
completion target was a directory, no separator would be added after it,
requiring us to manually append a `/` or `\`. Now, if the completion
target is a directory, a `/` or `\` will be automatically added. On
Windows, both `/` and `\` are considered valid path separators.



https://github.com/user-attachments/assets/0594ce27-9693-4a49-ae0e-3ed29f62526a



Release Notes:

- N/A
2025-03-04 14:01:08 +08:00
Joseph T. Lyons
8c4da9fba0 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.
2025-03-04 05:35:18 +00:00
Conrad Irwin
1f7fa80166 git: Fix race condition loading project diff (#25992)
Release Notes:

- git: Fixed a race condition where some files would be missing from
project diff
2025-03-03 21:40:37 -07:00
Conrad Irwin
2ac952ee6b 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
2025-03-03 21:40:20 -07:00
Cole Miller
495612be2e Revert "git: Use worktree paths in the panel (#25950)" (#25995)
This reverts commit e7b3b8bf03.

Release Notes:

- N/A
2025-03-04 04:20:41 +00:00
Conrad Irwin
1086b282b8 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.
2025-03-03 20:49:29 -07:00
Joseph T. Lyons
ffe2bed1e2 Refactor more code around commit button text (#25990)
Missed this when doing https://github.com/zed-industries/zed/pull/25988

Release Notes:

- N/A
2025-03-04 03:33:38 +00:00
Joseph T. Lyons
88940732ca Use same commit button text in panel and modal (#25988)
Release Notes:

- Fixed inconsistencies in commit button text between Git panel and modal.
2025-03-04 03:03:34 +00:00
Mikayla Maki
74fc52d5ce Git Beta: Fix a few cases of empty toasts showing up (#25985)
Improve parsing of git remote outputs

Release Notes:

- N/A
2025-03-04 02:16:50 +00:00
Conrad Irwin
2a7a4a80c6 Fix toggle fold in deleted hunk (#25967)
Updates #25835
Updates #25951

Closes #ISSUE

Release Notes:

- Fixed toggling folds from within deleted hunks
2025-03-03 18:51:09 -07:00
Finn Evers
c03bf1af36 gpui: Ensure hitbox is inserted when element has hover listener (#25981)
Currently, when an element has only a hover listener, the attached
listener will never trigger, because within the check for whether a
hitbox has to be inserted for the given element, this case it not
considered.
That leads to the behaviour as described in
https://github.com/zed-industries/zed/pull/25602#discussion_r1970720972,
where another event listener has to be attached to the element in order
for the hover listener to work.

This PR fixes the issue by ensuring that a hitbox is also inserted when
only a hover listener is attached to the element.

Release Notes:

- N/A
2025-03-04 01:46:18 +00:00
Max Brunsfeld
922aaa0534 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:36:13 -08:00
Marshall Bowers
fc5ff318e3 markdown: Change the copy icon to a check once copied (#25970)
This PR makes it so the copy icon on code blocks will change to a check
once the code block has been copied.

Release Notes:

- N/A
2025-03-04 00:14:26 +00:00
Cole Miller
e7b3b8bf03 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 18:32:03 -05:00
Conrad Irwin
6faa7cd722 Fix regex search colors (#25962)
In #25005 we added regex syntax highlighting to search; but the existing
regex grammar highlighted every character as a string which was hard to
read.

This flips so that characters are not highlighted, and brackets, etc.
are.
<img width="346" alt="Screenshot 2025-03-03 at 14 39 35"
src="https://github.com/user-attachments/assets/f7d3ae9c-fb5c-45eb-a5e9-41a330fbe940"
/>


Release Notes:

- Fixed regex search box being overly green
2025-03-03 15:55:07 -07:00
Marshall Bowers
0776fa8f31 markdown: Allow code blocks and tables to be horizontally scrollable (#25956)
This PR adds the ability for Markdown code blocks and tables to be made
horizontally scrollable.

This is a feature that the caller can opt in to.

Right now we're using it for the rendered Markdown in the Assistant 2
panel.

Release Notes:

- N/A
2025-03-03 22:52:59 +00:00
Marshall Bowers
7321c814ce gpui: Add restrict_scroll_to_axis to match web scrolling behavior (#25963)
This PR adds a new `restrict_scroll_to_axis` style to allow consumers to
opt-in to the scrolling behavior found on the web.

When this is enabled the behavior will be such that:

- Scrolling using the mouse wheel will only scroll the Y axis
- Scrolling using the mouse wheel with <kbd>Shift</kbd> held will only
scroll the X axis

This behavior is useful in scenarios where you have some
vertically-scrollable content that is interspersed with
horizontally-scrollable elements, as otherwise the scroll will be
constantly hijacked by the horizontally-scrollable elements while trying
to scroll up and down in the vertically-scrollable container.

I think that this behavior should be the default, but it's a bit of a
sweeping change to make all at once, so for now it remains opt-in.

Release Notes:

- N/A
2025-03-03 17:32:08 -05:00
Nate Butler
ac3cb3df05 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 22:00:32 +00:00
Conrad Irwin
0bd40da546 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>
2025-03-03 14:44:39 -07:00
Marshall Bowers
3bec4eb117 markdown: Add initial support for tables (#25954)
This PR adds initial support for displaying tables to the `markdown`
crate.

This allows us to render tables in Assistant 2:

| Before | After |
|
----------------------------------------------------------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="1309" alt="Screenshot 2025-03-03 at 1 39 39 PM"
src="https://github.com/user-attachments/assets/ad7ada01-f35d-4fcf-a20c-deb42b55b34e"
/> | <img width="1297" alt="Screenshot 2025-03-03 at 3 38 21 PM"
src="https://github.com/user-attachments/assets/9b771126-30a0-479b-8c29-f5f572936f56"
/> |

There are a few known issues that should be addressed as follow-ups:

- The horizontal scrolling within a table is linked with the scrolling
of the parent container (e.g., the Assistant 2 thread)
- Cells are currently cut off entirely when they are too wide, would be
nice to truncate them with an ellipsis

Release Notes:

- N/A
2025-03-03 21:01:26 +00:00
Kirill Bulatov
bf6cc2697a Do not detach reparse tasks (#25934)
Drop previous reparse task, if a new one is spawned.

Release Notes:

- N/A
2025-03-03 22:41:46 +02:00
Cole Miller
dc3158c8ce 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
2025-03-03 20:33:02 +00:00
Cole Miller
9e2b7bc5dc Fix missing hunks in project diff after revert (#25906)
Release Notes:

- N/A
2025-03-03 18:53:34 +00:00
Cole Miller
b774a4b8d1 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
2025-03-03 18:39:04 +00:00
Nate Butler
16ab8701a2 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 18:38:15 +00:00
Marshall Bowers
b2add8c803 assistant2: Restore tool uses when loading saved threads (#25942)
This PR makes it so tool uses are restored when loading saved threads in
Assistant 2.

Release Notes:

- N/A
2025-03-03 18:32:26 +00:00
Joseph T. Lyons
6635462f7b Bump Zed to v0.178 (#25939)
Release Notes:

-N/A
2025-03-03 12:48:51 -05:00
Marshall Bowers
81ff6f7a3c assistant2: Persist threads using serde_json instead of bincode (#25938)
This PR changes how we persist threads in Assistant2 to use `serde_json`
instead of `bincode` for the representation.

This makes the format more flexible to work with (and will allow for
using things like `#[serde(default)]`) if the schema changes over time.

Note: We have to bump the LMDB database version for this, so any threads
created before now will be gone.

Release Notes:

- N/A
2025-03-03 17:47:10 +00:00
Bennet Bo Fenner
669082dbe0 assistant2: Fix keyboard navigation issues when a picker is open (#25928)
This fixes:
- Bug: Using "up" in model selector triggers assistant2::FocusUp not
menu::SelectPrev
- Bug: Pressing arrow up/down in the model selector opened in the inline
assistant doesn't work
- Bug: Dismissing the model selector with Esc is not working
- Bug: Dismissing context pickers with Esc no longer working

Release Notes:

- N/A
2025-03-03 17:02:25 +01:00
Marshall Bowers
d5bc7b9a79 extension_cli: Make use of scrollbar_thumb.background a hard error (#25932)
This PR updates the extension CLI to make the use of
`scrollbar_thumb.background` in a theme a hard error.

We're working to eradicate usage of this theme property, so this will
prevent new extensions from being published that use it.

Release Notes:

- N/A
2025-03-03 15:55:15 +00:00
smit
8bb2739e28 keymap: Update Prev to Previous follow-up (#25931)
Follow-up for https://github.com/zed-industries/zed/pull/25909

Add three more action replacements:

```
1. "pane::ActivatePrevItem" -> "pane::ActivatePreviousItem"
2. "vim::MoveToPrev" -> "vim::MoveToPrevious"
3. "vim:MoveToPrevMatch" -> "vim:MoveToPreviousMatch" 
```

Release Notes:

- N/A
2025-03-03 21:19:25 +05:30
Peter Tripp
466be14b56 Revert "Use multi-line regex for '\s'" (#25926)
Reverts zed-industries/zed#19241
Closes: https://github.com/zed-industries/zed/issues/25901

Although `\s` contains `\n` it is widely used in non-multiline regexes (unlike `\n`).
2025-03-03 10:32:49 -05:00
Kirill Bulatov
95446195af 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
2025-03-03 15:04:46 +00:00
Max Brunsfeld
b34c0fd71b git_ui: Fix item heights in git panel (#25833)
- Fixes items slightly overlapping in the git panel
- Fixes commit button in the project diff not opening modal

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-03 09:39:24 -05:00
Danilo Leal
e0060b92cc assistant: Adjust slash command picker (#25920)
Mostly just fine-tuning its positioning. Other changes are mainly using
the Label's `buffer_font` method instead of using a div for that.

Release Notes:

- N/A
2025-03-03 11:09:49 -03:00
Danilo Leal
06bcc42652 Revert "assistant_context_editor: Close menus on send (#25440)" (#25916)
Reverting https://github.com/zed-industries/zed/pull/25440

This is a good change, but given the PR was open for a while, I guess it
didn't catch conflicts with main, and so it broke it. Will revert it for
now, to keep main fresh, but will look into adding this behavior back
again.

Release Notes:

- N/A
2025-03-03 12:40:01 +00:00
brian tan
f24c226af8 assistant_context_editor: Close menus on send (#25440)
Closes #ISSUE

Before:


https://github.com/user-attachments/assets/e63b6207-0c80-4fd6-99c0-febe3d639ba1

After:


https://github.com/user-attachments/assets/870f2c6d-9b7f-456d-a1e3-26e1c31b129d

Release Notes:

- N/A
2025-03-03 09:22:23 -03:00
smit
593f3dc1d5 keymap: Update Prev to Previous for consistency (#25909)
Closes #10167

This is take 2 on https://github.com/zed-industries/zed/pull/2341 which
was closed due to lack of migrator.

This PR contains rename of following keymap actions: 
```sh
1. ["editor::GoToPrevHunk", { "center_cursor": true }] -> ["editor::GoToPreviousHunk", { "center_cursor": true }]
2. "editor::GoToPrevDiagnostic" -> "editor::GoToPreviousDiagnostic"
3. "editor::ContextMenuPrev" -> "editor::ContextMenuPrevious"
4. "search::SelectPrevMatch" -> "search::SelectPreviousMatch"
5. "file_finder::SelectPrev" -> "file_finder::SelectPrevious"
6. "menu::SelectPrev" -> "menu::SelectPrevious"
7. "editor::TabPrev" -> "editor::Backtab"
```

Release Notes:

- Renamed several keymap actions for consistency (e.g., `GoToPrevHunk` →
`GoToPreviousHunk`, `TabPrev` → `Backtab`). Your existing configured
keybindings will still work. You can click **"Backup and Update"** at
the top of your keymap file to easily update to the new actions.


Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-03-03 17:44:49 +05:30
Danilo Leal
61d584db45 context menu: Adjust item disabled state when there is docs aside (#25860)
When a context menu item has a documentation aside element attached to
it, we're now hiding the keybinding (which wouldn't trigger anything
anyway) to make room for displaying an info icon, with the purpose of
indicating the existence of the docs aside, which will typically explain
the reason why the item’s disabled in the first place.

Also, changed the label color to use the `Disabled` token; more
appropriate for this, and just slightly darker, which is great!

<img
src="https://github.com/user-attachments/assets/a7f9f022-16d1-41d5-b1b5-3cbcc9630cc8"
width="500px"/>

Release Notes:

- N/A
2025-03-03 08:59:42 -03:00
Jason Lee
c37f616c3b gpui: Maintain img aspect ratio when max_width is set (#25632)
Release Notes:

- Fixed Markdown preview to display image with max width 100%.

## Before

<img width="1202" alt="image"
src="https://github.com/user-attachments/assets/359628df-8746-456f-a768-b3428923c937"
/>
<img width="750" alt="SCR-20250226-napv"
src="https://github.com/user-attachments/assets/f6154516-470e-41b2-84f5-ef0612c447ad"
/>


## After

<img width="1149" alt="image"
src="https://github.com/user-attachments/assets/2279347d-9c69-4a47-bb62-ccc8e55a98f6"
/>
<img width="520" alt="SCR-20250226-ngyz"
src="https://github.com/user-attachments/assets/03af5f14-1935-472e-822f-4c7f62630780"
/>
2025-03-03 12:36:27 +01:00
Mikayla Maki
73ac19958a Add user-visible output for remote operations (#25849)
This PR adds toasts for reporting success and errors from remote git
operations. This PR also adds a focus handle to notifications, in
anticipation of making them keyboard accessible.

Release Notes:

- N/A

---------

Co-authored-by: julia <julia@zed.dev>
2025-03-03 09:20:15 +00:00
Mikayla Maki
508b9d3b5d Add an informative tooltip to commit button when unable to commit (#25912)
Release Notes:

- N/A
2025-03-03 08:44:46 +00:00
Morgan Metz
0a4ff2f475 tab: Add setting to hide the close button entirely (#23880)
Closes #23744

Release Notes:

- Changed the `always_show_close_button` key to `show_close_button` and
introduced a new `hidden` value, that allows never displaying the close
button.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: smit <0xtimsb@gmail.com>
2025-03-03 08:31:48 +05:30
Max Brunsfeld
ae6d350334 Use system-installed git binary for push/pull/fetch (#25900)
### Problem

When using HTTPS remotes, users are getting errors when trying to push
or pull via the git panel.

On macOS, Zed bundles a `git` binary that's part of
[`dugite-native`](https://github.com/desktop/dugite-native). But we
don't include the entire package. Additional binaries from
`dugite-native` are needed for pulling and pushing over HTTPS.

### Solution

Rather than bundling those additional binaries, I've changed the `push`,
`pull`, and `fetch` actions to rely on the *system-installed* `git`
binary. The downside of this is that, if the user does not have Git
installed, they wont' be able to push, pull, or fetch from within Zed.
But we believe that the vast majority of users will have Git installed.
Also, unlike `diff` and `status`, which Zed needs to call in the
background without any user interaction, `push`/`pull` and `fetch` are
explicit actions that the user takes in Zed, so there is an opportunity
to prompt them to install Git if they haven't.

### Background

There are three ways (that I know of) that users might authenticate when
pushing, pulling, or fetching over HTTPS.

1. Via a built-in [Git
`credential.helper`](https://git-scm.com/docs/gitcredentials). On macOS,
Git ships with a helper called `credential-osxkeychain` that stores
internet passwords in the OS Keychain. You can opt into this globally
with the command `git config --global credential.helper osxkeychain`,
which writes to your `~/.gitconfig`.
2. Via [`Git Credential Manager`
(GCM)](https://github.com/git-ecosystem/git-credential-manager), which
is a different `credential.helper`, [built by
GitHub](https://github.blog/security/application-security/git-credential-manager-authentication-for-everyone/),
which must be installed manually, and integrates with specific Git
hosting providers like GitHub and Azure.
3. By typing their Username and Password/Access-token interactively when
pushing/pulling/fetching.

### Testing Status

* [ ] 🚫 Interactive password auth - not yet supported, requires
https://github.com/zed-industries/zed/pull/25848
* [x] **credential-osxkeychain** - when using the built-in credential
helper, and the credentials are already stored in the keychain,
push/pull/fetch now work fine .
* [ ] **GCM**  - still testing.
* Right now, I'm seeing `git-credential-manager` just hang indefinitely
when pushing from Zed, even though it works when pushing from a
terminal.



Release Notes:

- N/A
2025-03-02 17:31:47 -08:00
Piotr Osiewicz
0e44f93178 lsp: Do not add trailing slash to workspace folders (#25903)
Closes #25390

Release Notes:

- Fixed issues with ansible-language-server sending phantom diagnostic
updates
2025-03-02 22:09:13 +00:00
Joseph T. Lyons
65d92d7278 Make more log files read only (#25887)
Closes https://github.com/zed-industries/zed/issues/5105

Makes the following logs read only

- Zed error / warning log
- Zed telemetry log

Release Notes:

- N/A
2025-03-02 08:05:55 +00:00
Devzeth
8b5ef2558b docs: Add documentation for Seed search query from cursor (#25875)
Missing documentation added for `seed_search_query_from_cursor`. 

Release Notes:

- N/A
2025-03-02 08:46:22 +02:00
Ben Kunkle
fec228bb23 Fix issue with cmd-w closing window in preview tabs on MacOS (#25878)
Closes #25810

Reorders default macOS keymap so that `cmd-w` in `"context":
"PromptLibrary"` bindings is not the last binding for
`workspace::CloseWindow` and therefore does not get rendered in the app
menu or intercepted by MacOS

Release Notes:

- N/A
2025-03-01 19:33:01 -06:00
Devzeth
e00d737196 Add constructor highlighting for JS/TS/TSX (#25207)
Closes #19267

Adds highlight field specifically for `constructor` in JS/TS/TSX so that
it can be highlighted as a different color than regular methods

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-03-02 00:43:51 +00:00
Egor Krugletsov
b0dee94126 pane: Hide "Copy Relative Path" and "Reveal In Project Panel" actions for files outside of the projects (#25386)
"Copy Relative Path" action had a check for existence of relative path
but it always passed because
[`WorktreeStore::find_worktree()`](1d5499bee7/crates/project/src/worktree_store.rs (L148))
function returned empty path for these kinds of files. It feels correct
to make changes there, but I don't know what else could be impacted.

"Reveal In Project Panel" had no check whatsoever and so I made one.

Release Notes:

- N/A
2025-03-01 23:31:18 +02:00
greathongtu
e65471c7a1 Optimize JSON merging by removing redundant key clones in serde_json operations (#25866)
Hi Zed team! 👋
As a fan of Zed Editor who's excited to contribute, I noticed a small
optimization opportunity in the JSON merging utilities.

<b>Changes:</b>
Removed redundant key.clone() calls in insert() operations within 2
functions:
1. merge_json_value_into
2. merge_non_null_json_value_into

<b>Why:</b>
Since we're already moving ownership of `source_object` 's contents and
key is no longer used further, we could directly pass `key` to `insert`
without cloning.
Eliminates redundant allocations, improving performance slightly for
JSON-heavy operations.

<b>Testing:</b>
I have tested this locally and all existing tests passed. The change
preserves behavior while removing redundancy.

Love using Zed and happy to contribute! Let me know if any adjustments
are needed!

Release Notes:

- N/A
2025-03-01 14:13:38 -05:00
张小白
6713ec8cdf windows: Bring back restoration of tabs (#25870)
Closes #25022

Release Notes:

- N/A
2025-03-01 17:18:34 +00:00
Joseph T. Lyons
48e09c0026 Tweak the Git beta issue template (#25869)
Release Notes:

- N/A
2025-03-01 11:33:42 -05:00
Joseph T. Lyons
3f03d7b023 Add a temporary issue template for Git beta bugs (#25867)
Release Notes:

- N/A
2025-03-01 11:31:25 -05:00
Devzeth
fa96e2259b paths: Add support for clickable file paths in the Odin language format (#25842)
This PR adds support for clickable file paths in the Odin language format.

The odin compiler errors use the format `/path/to/file.odin(1:1)`. We
didn't recognize this format, making these paths non-clickable in the
terminal.

Also added tests for this. 


Release Notes:

- Added support for clickable file paths in the Odin language format.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-03-01 16:22:12 +00:00
Maksim Bondarenkov
a8a05f208b cli: Add extra paths in detect() on Windows (#25765)
I'm already integrating CLI into MSYS2 package, and this patche helped
me to make it work like in Arch:
https://github.com/msys2/MINGW-packages/pull/23537. used the same way as
in [Linux
implementation](6856e869fc/crates/cli/src/main.rs (L314))

Closes #ISSUE

Release Notes:

- N/A
2025-03-02 00:17:55 +08:00
Peter Tripp
aa1ab50656 Add stop_at_indent for Editor::DeleteToBeginningOfLine (#25688)
Added test_beginning_of_line_stop_at_indent editor test

- Follow-up to: https://github.com/zed-industries/zed/pull/25428
- Replaces: https://github.com/zed-industries/zed/pull/25346

This is all authored by @felixpackard in #25346
I just updated it to use `stop_at_indent` instead of
`stop_at_first_char`.

Release Notes:

- Added support for `stop_at_indent` to
`Editor::DeleteToBeginningOfLine` (thanks
[@felixpackard](https://github.com/felixpackard))

Co-authored-by: Felix Packard <felix@rigr.gg>
2025-03-01 11:03:47 -05:00
张小白
d115cb1944 windows: Use dev drive instead of ReFS (#25858)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-01 22:43:10 +08:00
Kirill Bulatov
42571e405f Ensure inlay hint toggling with modifiers happens fast (#25852)
Follow-up of https://github.com/zed-industries/zed/pull/25766 
Fixes the bugs found:

* modifier toggle not happening instantly due to `edit_debounce_ms`
considered
* hint update race that ignored the cache clear

Release Notes:

- N/A
2025-03-01 08:15:57 +00:00
João Marcos
2d61a51ded Diff View: Scroll to center of hunks when reviewing (#25846)
When reviewing hunks, scroll to put them at the center of the screen
so you can better see the context around that hunk.

The field `center_cursor` was added to the actions `editor::GoToHunk`
and `editor::GoToPrevHunk`, this was set to `false` by default in
keymaps, as it wouldn't help with in-editor navigation.

The field is set to `true` for when you trigger `git::StageAndNext`
and `git::UnstageAndNext`, this is also `true` for the buttons in the
Diff View toolbar.

Release Notes:

- N/A
2025-03-01 03:20:26 +00:00
João Marcos
a2876f5d3e Support hunk-wise StageAndNext and UnstageAndNext (#25845)
This PR adds the `whole_excerpt` field to the actions:

- `git::StageAndNext`
- `git::UnstageAndNext`

Which is set by false by default, effectively, now staging and unstaging
with these actions is done hunk-by-hunk, this also affects the `Stage`
and
`Unstage` buttons in the Diff View toolbar.

A caveat: with this PR, there is no way to configure the buttons in the
Diff
View toolbar to restore the previous behavior, if we want, I think we
can make
it a setting, but let's see if anyone really wants that.

Release Notes:

- N/A
2025-03-01 02:39:08 +00:00
Cole Miller
13deaa3f69 Fix new git panel buttons (#25844)
Release Notes:

- N/A
2025-02-28 21:19:20 -05:00
Ben Kunkle
694afd15c9 Fix buffer search options not resetting when dismissed after Vim mode search then reopened with buffer: deploy search (#25838)
Closes #25315

Release Notes:

- Fixes an issue where the buffer search options would not be reset when
using `buffer: deploy search` after using Vim search (`*` & `#`) which
enable all search options
2025-02-28 18:59:22 -06:00
Cole Miller
eb648dd096 Follow-up tweaks to new git panel footer (#25832)
- Use a popover for the branch picker
- Don't deploy a repository selector if there's only one repo

Release Notes:

- N/A
2025-02-28 19:05:43 -05:00
Max Brunsfeld
1c4c568068 Allow unfolding deleted buffers in project diff w/ keyboard (#25835)
Release Notes:

- N/A
2025-02-28 16:02:35 -08:00
Ben Kunkle
ec88a6886f Fix active pane modifiers applying to parent pane axis if child pane is active (#25836)
Closes #25304

Release Notes:

- Fixed an issue where `active_pane_modifiers` settings would be applied
to a parent pane if one of it's child panes was active
2025-02-28 23:47:15 +00:00
Piotr Osiewicz
7fb16977ce chore: Extract PromptStore out of prompt_library (#25837)
One step closer to removing long pole with assistant/assistant2 builds

Release Notes:

- N/A
2025-03-01 00:34:28 +01:00
Peter Tripp
53b2792844 Improve script/mitm-proxy.sh to support podman (#25834) 2025-02-28 22:37:03 +00:00
Peter Tripp
760d08711c Update bundled JSON schemas (2025-02-28) (#25826)
Updated JSON schemas to
[SchemaStore/schemastore@b107b83](b107b83a50)
(2025-02-28)

-
[tsconfig.json](https://github.com/SchemaStore/schemastore/commits/master/src/schemas/json/tsconfig.json)
@
[b107b83](https://raw.githubusercontent.com/SchemaStore/schemastore/b107b83a50bad9ac06dd171201dc870901f92ca8/src/schemas/json/tsconfig.json)
-
[package.json](https://github.com/SchemaStore/schemastore/commits/master/src/schemas/json/package.json)
@
[b107b83](https://raw.githubusercontent.com/SchemaStore/schemastore/b107b83a50bad9ac06dd171201dc870901f92ca8/src/schemas/json/package.json)

Previously:
- https://github.com/zed-industries/zed/pull/20910

Release Notes:

- Updated bundled JSON schemas for package.json and tsconfig.json
2025-02-28 16:32:16 -05:00
Marshall Bowers
71866d6314 assistant2: Include some text in the tool result messages (#25825)
This PR makes it so we include some textual content in the user messages
that as used to send up tool results.

I observed that when sending up the tool results with no text, it would
lead the model to start replying with no text, which would then result
in an error when attaching later tool results.

I think there's a deeper issue at play here, but for now we just include
some text to keep the model on track.

Release Notes:

- N/A
2025-02-28 21:02:03 +00:00
Max Brunsfeld
0c2bbb3aa9 Optimistically update hunk states when staging and unstaging hunks (#25687)
This PR adds an optimistic update when staging or unstaging diff hunks.
In the process, I've also refactored the logic for staging and unstaging
hunks, to consolidate more of it in the `buffer_diff` crate.

I've also changed the way that we treat untracked files. Previously, we
maintained an empty diff for them, so as not to show unwanted
entire-file diff hunks in a regular editor. But then in the project diff
view, we had to account for this, and replace these empty diffs with
entire-file diffs. This form of state management made it more difficult
to store the pending hunks, so now we always use the same
`BufferDiff`/`BufferDiffSnapshot` for untracked files (with a single
hunk spanning the entire buffer), but we just have a special case in
regular buffers, that avoids showing that entire-file hunk.

* [x] Avoid creating a long queue of `set_index` operations when
staging/unstaging rapidly
* [x] Keep pending hunks when diff is recalculated without base text
changes
* [x] Be optimistic even when staging the single hunk in added/deleted
files
* Testing

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-02-28 20:55:29 +00:00
Nate Butler
9d8a163f5b git_ui: New panel design (#25821)
This PR updates the ui of the git panel. It removes the header from the
panel and unifies the repository, branch and commit controls in the
bottom section.

It also adds a secondary menu to the primary button giving access to a
variety of actions for managing local and remote changes:

![CleanShot 2025-02-28 at 12 18
15@2x](https://github.com/user-attachments/assets/0260c122-405f-46fc-8cc8-d6beac782b9d)

Known issues (will be fixed in a later pr)
- Spinner showing git operation progress was removed, will be re-added
- Clicking expand with the panel editor focused will commit (due to
shared action name. Already tracked)

Before | After

![CleanShot 2025-02-28 at 12 22
18@2x](https://github.com/user-attachments/assets/4c1e4ac9-b975-487f-bf4e-8815a8da4f4f)

(Also adds `component`, `linkme` to cargo-machete ignore as they are
used in the `IntoComponent` proc-macro and will always be incorrectly
flagged as unused)

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-28 20:00:39 +00:00
Nate Butler
8a22a07d14 git: Adjust rendering of git hunks (#25824)
- Light themes get their own values (creating better contrast and a
better distinction between staged and unstaged hunks in light themes.)
- Scrollbar git hunks indicators now use the correct colors

Before:

![CleanShot 2025-02-28 at 14 31
29@2x](https://github.com/user-attachments/assets/038fe11c-7163-4f1b-92b8-56b24c8e9443)

After:

![CleanShot 2025-02-28 at 14 32
04@2x](https://github.com/user-attachments/assets/869d33d9-d925-4cbe-84bd-e54caf971431)


Release Notes:

- Fixed an issue where git hunk indicators in editor scrollbars used the
incorrect colors.
2025-02-28 19:54:12 +00:00
smit
fad4df5e70 editor: Add Organize Imports Action (#25793)
Closes #10004

This PR adds support for the organize imports action. Previously, you
had to manually configure it in the settings and then use format to run
it.

Note: Default key binding will be `alt-shift-o` which is similar to
VSCode's organize import. Also, because `cmd-shift-o` is taken by
outline picker.

Todo:

- [x] Initial working
- [x] Handle remote
- [x] Handle multi buffer
- [x] Can we make it generic for executing any code action?

Release Notes:

- Added `editor:OrganizeImports` action to organize imports (sort,
remove unused, etc) for supported LSPs. You can trigger it by using the
`alt-shift-o` key binding.
2025-03-01 00:59:09 +05:30
242 changed files with 9477 additions and 3184 deletions

View File

@@ -0,0 +1,51 @@
name: Git Beta
description: There is a bug related to new Git features in Zed
type: "Bug"
labels: [git]
title: "Git Beta: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
macOS: `~/Library/Logs/Zed/Zed.log`
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary>
<!-- Click below this line and paste or drag-and-drop your log-->
```
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
validations:
required: false

274
Cargo.lock generated
View File

@@ -405,6 +405,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_library",
"prompt_store",
"proto",
"rand 0.8.5",
"rope",
@@ -472,6 +473,7 @@ dependencies = [
"picker",
"project",
"prompt_library",
"prompt_store",
"proto",
"rand 0.8.5",
"rope",
@@ -526,7 +528,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
"prompt_library",
"prompt_store",
"rand 0.8.5",
"regex",
"rope",
@@ -617,7 +619,7 @@ dependencies = [
"log",
"pretty_assertions",
"project",
"prompt_library",
"prompt_store",
"rope",
"schemars",
"semantic_index",
@@ -642,9 +644,11 @@ dependencies = [
"collections",
"derive_more",
"gpui",
"language",
"parking_lot",
"serde",
"serde_json",
"ui",
"workspace",
]
@@ -1176,9 +1180,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.5.17"
version = "1.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490aa7465ee685b2ced076bb87ef654a47724a7844e2c7d3af4e749ce5b875dd"
checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1269,9 +1273,9 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.75.0"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ddf7475b6f50a1a5be8edb1bcdf6e4ae00feed5b890d14a3f1f0e14d76f5a16"
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1293,9 +1297,9 @@ dependencies = [
[[package]]
name = "aws-sdk-kinesis"
version = "1.62.0"
version = "1.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31622345afd0c35d33c1cbba73ccf9fb88e09857413d8963dea2c493e00704d"
checksum = "89f2163d8704e8fdcd51ec6c2e0441c418471e422ee9690451b17a1c46344e1a"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1315,9 +1319,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.77.0"
version = "1.76.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34e87342432a3de0e94e82c99a7cbd9042f99de029ae1f4e368160f9e9929264"
checksum = "66e83401ad7287ad15244d557e35502c2a94105ca5b41d656c391f1a4fc04ca2"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1349,9 +1353,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.60.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60186fab60b24376d3e33b9ff0a43485f99efd470e3b75a9160c849741d63d56"
checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1371,9 +1375,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7033130ce1ee13e6018905b7b976c915963755aef299c1521897679d6cd4f8ef"
checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1393,9 +1397,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5c1cac7677179d622b4448b0d31bcb359185295dc6fca891920cfb17e2b5156"
checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1456,9 +1460,9 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.63.0"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2dc8d842d872529355c72632de49ef8c5a2949a4472f10e802f28cf925770c"
checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@@ -1808,7 +1812,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -1831,7 +1835,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
@@ -2076,6 +2080,7 @@ name = "buffer_diff"
version = "0.1.0"
dependencies = [
"anyhow",
"clock",
"ctor",
"env_logger 0.11.6",
"futures 0.3.31",
@@ -2401,6 +2406,25 @@ dependencies = [
"cipher",
]
[[package]]
name = "cbindgen"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
dependencies = [
"clap",
"heck 0.4.1",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.90",
"tempfile",
"toml 0.8.20",
]
[[package]]
name = "cbindgen"
version = "0.28.0"
@@ -2498,9 +2522,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.40"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -2508,7 +2532,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-targets 0.52.6",
]
[[package]]
@@ -2841,7 +2865,7 @@ dependencies = [
"pretty_assertions",
"project",
"prometheus",
"prompt_library",
"prompt_store",
"prost 0.9.0",
"rand 0.8.5",
"recent_projects",
@@ -3505,10 +3529,11 @@ dependencies = [
[[package]]
name = "crc64fast-nvme"
version = "1.2.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37"
dependencies = [
"cbindgen 0.27.0",
"crc",
]
@@ -4287,6 +4312,12 @@ dependencies = [
"regex",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.10.2"
@@ -5396,8 +5427,11 @@ dependencies = [
"anyhow",
"buffer_diff",
"collections",
"component",
"ctor",
"db",
"editor",
"env_logger 0.11.6",
"feature_flags",
"futures 0.3.31",
"fuzzy",
@@ -5405,6 +5439,9 @@ dependencies = [
"gpui",
"itertools 0.14.0",
"language",
"linkify",
"linkme",
"log",
"menu",
"multi_buffer",
"panel",
@@ -5416,6 +5453,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"smallvec",
"strum",
"theme",
"time",
@@ -5555,7 +5593,7 @@ dependencies = [
"bytemuck",
"calloop",
"calloop-wayland-source",
"cbindgen",
"cbindgen 0.28.0",
"cocoa 0.26.0",
"collections",
"core-foundation 0.9.4",
@@ -7228,9 +7266,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libdbus-sys"
@@ -7635,6 +7673,25 @@ dependencies = [
"url",
]
[[package]]
name = "lua-src"
version = "547.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.5.12+a4f56a4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
dependencies = [
"cc",
"which 7.0.2",
]
[[package]]
name = "lyon"
version = "1.0.1"
@@ -8048,6 +8105,33 @@ dependencies = [
"strum",
]
[[package]]
name = "mlua"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b"
dependencies = [
"bstr",
"either",
"mlua-sys",
"num-traits",
"parking_lot",
"rustc-hash 2.1.1",
]
[[package]]
name = "mlua-sys"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594"
dependencies = [
"cc",
"cfg-if",
"lua-src",
"luajit-src",
"pkg-config",
]
[[package]]
name = "msvc_spectre_libs"
version = "0.1.2"
@@ -9757,15 +9841,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "pgvector"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
dependencies = [
"serde",
]
[[package]]
name = "phf"
version = "0.11.2"
@@ -10310,12 +10385,36 @@ dependencies = [
[[package]]
name = "prompt_library"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"gpui",
"language",
"language_model",
"log",
"menu",
"picker",
"prompt_store",
"release_channel",
"rope",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "prompt_store"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"chrono",
"collections",
"editor",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -10323,23 +10422,14 @@ dependencies = [
"handlebars 4.5.0",
"heed",
"language",
"language_model",
"log",
"menu",
"parking_lot",
"paths",
"picker",
"release_channel",
"rope",
"serde",
"settings",
"text",
"theme",
"ui",
"util",
"uuid",
"workspace",
"zed_actions",
]
[[package]]
@@ -10390,7 +10480,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.0",
"heck 0.5.0",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10423,7 +10513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -11451,9 +11541,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -11462,9 +11552,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
dependencies = [
"proc-macro2",
"quote",
@@ -11475,9 +11565,9 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
dependencies = [
"globset",
"sha2",
@@ -11752,9 +11842,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"indexmap",
@@ -11765,9 +11855,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
@@ -11793,6 +11883,28 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "scripting_tool"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_tool",
"collections",
"gpui",
"language",
"mlua",
"parking_lot",
"regex",
"rich_text",
"schemars",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"workspace",
]
[[package]]
name = "scrypt"
version = "0.11.0"
@@ -11830,18 +11942,17 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fba7b2c749b2d0a00303d5cb13e6761e39a4172554bdf930852cac4e7aeabd"
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
dependencies = [
"async-stream",
"async-trait",
"bigdecimal",
"chrono",
"futures-util",
"futures 0.3.31",
"log",
"ouroboros",
"pgvector",
"rust_decimal",
"sea-orm-macros",
"sea-query",
@@ -11859,9 +11970,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2568cff8d35d5150b4276cc0dd766192a587f64b6ece60ae3706e0872c4eb209"
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -14781,9 +14892,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.15.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
dependencies = [
"getrandom 0.3.1",
"serde",
@@ -14885,6 +14996,8 @@ dependencies = [
"multi_buffer",
"nvim-rs",
"parking_lot",
"picker",
"project",
"project_panel",
"regex",
"release_channel",
@@ -15687,6 +15800,18 @@ dependencies = [
"winsafe",
]
[[package]]
name = "which"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
dependencies = [
"either",
"env_home",
"rustix",
"winsafe",
]
[[package]]
name = "whoami"
version = "1.5.2"
@@ -15906,12 +16031,6 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -16747,7 +16866,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.177.0"
version = "0.178.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -16825,7 +16944,7 @@ dependencies = [
"project",
"project_panel",
"project_symbols",
"prompt_library",
"prompt_store",
"proto",
"recent_projects",
"release_channel",
@@ -16833,6 +16952,7 @@ dependencies = [
"repl",
"reqwest_client",
"rope",
"scripting_tool",
"search",
"serde",
"serde_json",
@@ -16851,7 +16971,6 @@ dependencies = [
"tasks_ui",
"telemetry",
"telemetry_events",
"tempfile",
"terminal_view",
"theme",
"theme_extension",
@@ -16868,7 +16987,6 @@ dependencies = [
"vim",
"vim_mode_setting",
"welcome",
"which 6.0.3",
"windows 0.58.0",
"winresource",
"workspace",

View File

@@ -103,6 +103,7 @@ members = [
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_library",
"crates/prompt_store",
"crates/proto",
"crates/recent_projects",
"crates/refineable",
@@ -116,6 +117,7 @@ members = [
"crates/rope",
"crates/rpc",
"crates/schema_generator",
"crates/scripting_tool",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
@@ -308,6 +310,7 @@ project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
prompt_library = { path = "crates/prompt_library" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
@@ -319,6 +322,7 @@ 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" }
@@ -451,6 +455,7 @@ 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"
@@ -749,4 +754,4 @@ should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]

1
assets/icons/cloud.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,6 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,12 +1,5 @@
<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 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>

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -10,8 +10,8 @@
"pagedown": "menu::SelectLast",
"ctrl-n": "menu::SelectNext",
"tab": "menu::SelectNext",
"ctrl-p": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"ctrl-p": "menu::SelectPrevious",
"shift-tab": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
@@ -38,14 +38,14 @@
{
"context": "Picker || menu",
"bindings": {
"up": "menu::SelectPrev",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrev",
"left": "menu::SelectPrevious",
"right": "menu::SelectNext"
}
},
@@ -57,7 +57,7 @@
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"shift-tab": "editor::Backtab",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"ctrl-k ctrl-q": "editor::Rewrap",
@@ -180,7 +180,7 @@
"ctrl-k c": "assistant::CopyCode",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary",
@@ -203,7 +203,7 @@
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"shift-enter": "search::SelectPreviousMatch",
"alt-enter": "search::SelectAllMatches",
"find": "search::FocusSearch",
"ctrl-f": "search::FocusSearch",
@@ -272,7 +272,7 @@
"alt-8": ["pane::ActivateItem", 7],
"alt-9": ["pane::ActivateItem", 8],
"alt-0": "pane::ActivateLastItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight",
@@ -290,8 +290,8 @@
"forward": "pane::GoForward",
"ctrl-alt-g": "search::SelectNextMatch",
"f3": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
"shift-f3": "search::SelectPreviousMatch",
"shift-find": "project_search::ToggleFocus",
"ctrl-shift-f": "project_search::ToggleFocus",
"ctrl-alt-shift-h": "search::ToggleReplace",
@@ -334,7 +334,7 @@
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPrevDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
@@ -373,7 +373,7 @@
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPrevHunk"
"alt-,": "editor::GoToPreviousHunk"
}
},
{
@@ -536,8 +536,8 @@
{
"context": "Editor && (showing_code_actions || showing_completions)",
"bindings": {
"ctrl-p": "editor::ContextMenuPrev",
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrevious",
"up": "editor::ContextMenuPrevious",
"ctrl-n": "editor::ContextMenuNext",
"down": "editor::ContextMenuNext",
"pageup": "editor::ContextMenuFirst",
@@ -565,7 +565,7 @@
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
"ctrl-shift-f8": "editor::GoToPreviousHunk",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-:": "editor::ToggleInlayHints"
}
@@ -612,12 +612,29 @@
"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": {
@@ -662,7 +679,7 @@
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",
"ctrl-alt-enter": "editor::OpenExcerptsSplit"
}
@@ -700,7 +717,7 @@
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
}
},
@@ -713,7 +730,7 @@
{
"context": "GitPanel && ChangesList",
"bindings": {
"up": "menu::SelectPrev",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"space": "git::ToggleStaged",
@@ -722,7 +739,7 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"ctrl-enter": "git::ShowCommitEditor",
"alt-enter": "menu::SecondaryConfirm"
}
},
@@ -736,7 +753,7 @@
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit"
"ctrl-enter": "git::ShowCommitEditor"
}
},
{
@@ -749,14 +766,6 @@
"alt-up": "git_panel::FocusChanges"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"ctrl-enter": "git::Commit"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {
@@ -779,6 +788,9 @@
{
"context": "Picker > Editor",
"bindings": {
"escape": "menu::Cancel",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"tab": "picker::ConfirmCompletion",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }]
}
@@ -792,7 +804,7 @@
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrev",
"ctrl-shift-p": "file_finder::SelectPrevious",
"ctrl-j": "pane::SplitDown",
"ctrl-k": "pane::SplitUp",
"ctrl-h": "pane::SplitLeft",
@@ -802,8 +814,8 @@
{
"context": "TabSwitcher",
"bindings": {
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-up": "menu::SelectPrev",
"ctrl-shift-tab": "menu::SelectPrevious",
"ctrl-up": "menu::SelectPrevious",
"ctrl-down": "menu::SelectNext",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}

View File

@@ -1,4 +1,15 @@
[
// Moved before Standard macOS bindings so that `cmd-w` is not the last binding for
// `workspace::CloseWindow` and displayed/intercepted by macOS
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
}
},
// Standard macOS bindings
{
"use_key_equivalents": true,
@@ -14,19 +25,19 @@
"tab": "menu::SelectNext",
"ctrl-n": "menu::SelectNext",
"down": "menu::SelectNext",
"shift-tab": "menu::SelectPrev",
"ctrl-p": "menu::SelectPrev",
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrevious",
"ctrl-p": "menu::SelectPrevious",
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"cmd-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 }],
@@ -54,7 +65,7 @@
"ctrl-d": "editor::Delete",
"delete": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"shift-tab": "editor::Backtab",
"ctrl-t": "editor::Transpose",
"ctrl-k": "editor::KillRingCut",
"ctrl-y": "editor::KillRingYank",
@@ -118,6 +129,7 @@
"cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
@@ -207,7 +219,7 @@
"cmd-k c": "assistant::CopyCode",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary",
@@ -244,6 +256,14 @@
"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,
@@ -251,6 +271,15 @@
"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,
@@ -269,15 +298,6 @@
"backspace": "assistant2::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
}
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
@@ -285,7 +305,7 @@
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"shift-enter": "search::SelectPreviousMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace",
@@ -352,8 +372,8 @@
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"alt-cmd-left": "pane::ActivatePrevItem",
"cmd-{": "pane::ActivatePrevItem",
"alt-cmd-left": "pane::ActivatePreviousItem",
"cmd-{": "pane::ActivatePreviousItem",
"alt-cmd-right": "pane::ActivateNextItem",
"cmd-}": "pane::ActivateNextItem",
"ctrl-shift-pageup": "pane::SwapItemLeft",
@@ -367,7 +387,7 @@
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-shift-h": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
@@ -407,7 +427,7 @@
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPrevDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
@@ -613,8 +633,8 @@
"context": "Editor && (showing_code_actions || showing_completions)",
"use_key_equivalents": true,
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
"up": "editor::ContextMenuPrevious",
"ctrl-p": "editor::ContextMenuPrevious",
"down": "editor::ContextMenuNext",
"ctrl-n": "editor::ContextMenuNext",
"pageup": "editor::ContextMenuFirst",
@@ -640,7 +660,7 @@
"cmd-alt-enter": "editor::OpenExcerptsSplit",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPrevHunk",
"cmd-shift-f8": "editor::GoToPreviousHunk",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-:": "editor::ToggleInlayHints"
}
@@ -683,7 +703,7 @@
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",
"cmd-alt-enter": "editor::OpenExcerptsSplit"
}
@@ -713,7 +733,7 @@
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
}
},
@@ -728,7 +748,7 @@
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
@@ -740,14 +760,14 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit"
"cmd-enter": "git::ShowCommitEditor"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit"
"cmd-enter": "git::ShowCommitEditor"
}
},
{
@@ -795,6 +815,9 @@
"context": "Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"tab": "picker::ConfirmCompletion",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
@@ -811,7 +834,7 @@
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd-shift-p": "file_finder::SelectPrevious",
"cmd-j": "pane::SplitDown",
"cmd-k": "pane::SplitUp",
"cmd-h": "pane::SplitLeft",
@@ -822,8 +845,8 @@
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-up": "menu::SelectPrev",
"ctrl-shift-tab": "menu::SelectPrevious",
"ctrl-up": "menu::SelectPrevious",
"ctrl-down": "menu::SelectNext",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}

View File

@@ -39,7 +39,7 @@
"context": "BufferSearchBar",
"bindings": {
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
}
},
{

View File

@@ -122,7 +122,7 @@
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-r": "search::SelectPreviousMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},

View File

@@ -2,7 +2,7 @@
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePrevItem",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem"
}
},
@@ -41,9 +41,9 @@
"ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"shift-f2": "editor::GoToPreviousDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
"ctrl-alt-z": "git::Restore",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",

View File

@@ -1,9 +1,9 @@
[
{
"bindings": {
"ctrl-{": "pane::ActivatePrevItem",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-1": ["workspace::ActivatePane", 0],
"ctrl-2": ["workspace::ActivatePane", 1],
@@ -44,7 +44,7 @@
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
@@ -62,7 +62,7 @@
"context": "Pane",
"bindings": {
"f4": "search::SelectNextMatch",
"shift-f4": "search::SelectPrevMatch",
"shift-f4": "search::SelectPreviousMatch",
"alt-1": ["pane::ActivateItem", 0],
"alt-2": ["pane::ActivateItem", 1],
"alt-3": ["pane::ActivateItem", 2],

View File

@@ -40,7 +40,7 @@
"context": "BufferSearchBar",
"bindings": {
"cmd-f3": "search::SelectNextMatch",
"cmd-shift-f3": "search::SelectPrevMatch"
"cmd-shift-f3": "search::SelectPreviousMatch"
}
},
{

View File

@@ -122,7 +122,7 @@
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-r": "search::SelectPreviousMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},

View File

@@ -1,7 +1,7 @@
[
{
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-{": "pane::ActivatePreviousItem",
"cmd-}": "pane::ActivateNextItem"
}
},
@@ -39,9 +39,9 @@
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"shift-f2": "editor::GoToPreviousDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
"cmd-home": "editor::MoveToBeginning",
"cmd-end": "editor::MoveToEnd",
"cmd-shift-home": "editor::SelectToBeginning",
@@ -61,7 +61,7 @@
{
"context": "BufferSearchBar > Editor",
"bindings": {
"shift-enter": "search::SelectPrevMatch"
"shift-enter": "search::SelectPreviousMatch"
}
},
{

View File

@@ -1,9 +1,9 @@
[
{
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-{": "pane::ActivatePreviousItem",
"cmd-}": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-1": ["workspace::ActivatePane", 0],
"ctrl-2": ["workspace::ActivatePane", 1],
@@ -45,7 +45,7 @@
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
"alt-shift-cmd-down": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
@@ -64,7 +64,7 @@
"context": "Pane",
"bindings": {
"f4": "search::SelectNextMatch",
"shift-f4": "search::SelectPrevMatch",
"shift-f4": "search::SelectPreviousMatch",
"cmd-1": ["pane::ActivateItem", 0],
"cmd-2": ["pane::ActivateItem", 1],
"cmd-3": ["pane::ActivateItem", 2],

View File

@@ -47,7 +47,7 @@
"context": "BufferSearchBar",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-shift-s": "search::SelectPrevMatch"
"ctrl-shift-s": "search::SelectPreviousMatch"
}
},
{

View File

@@ -13,9 +13,9 @@
"tab": "menu::SelectNext",
"ctrl-n": "menu::SelectNext",
"down": "menu::SelectNext",
"shift-tab": "menu::SelectPrev",
"ctrl-p": "menu::SelectPrev",
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrevious",
"ctrl-p": "menu::SelectPrevious",
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",

View File

@@ -62,9 +62,9 @@
"g /": "pane::DeploySearch",
"?": ["vim::Search", { "backwards": true }],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
"#": "vim::MoveToPrevious",
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"shift-n": "vim::MoveToPreviousMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
@@ -106,7 +106,7 @@
"g g": "vim::StartOfDocument",
"g h": "editor::Hover",
"g t": "pane::ActivateNextItem",
"g shift-t": "pane::ActivatePrevItem",
"g shift-t": "pane::ActivatePreviousItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToDeclaration",
"g y": "editor::GoToTypeDefinition",
@@ -126,7 +126,7 @@
"g shift-a": "editor::FindAllReferences", // zed specific
"g space": "editor::OpenExcerpts", // zed specific
"g *": ["vim::MoveToNext", { "partial_word": true }],
"g #": ["vim::MoveToPrev", { "partial_word": true }],
"g #": ["vim::MoveToPrevious", { "partial_word": true }],
"g j": ["vim::Down", { "display_lines": true }],
"g down": ["vim::Down", { "display_lines": true }],
"g k": ["vim::Up", { "display_lines": true }],
@@ -138,7 +138,7 @@
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g [": "editor::GoToPreviousDiagnostic",
"g i": "vim::InsertAtPrevious",
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
@@ -231,15 +231,15 @@
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPrevHunk",
"[ c": "editor::GoToPreviousHunk",
"g c": "vim::PushToggleComments"
}
},
@@ -272,7 +272,7 @@
"shift-s": "vim::SubstituteLine",
"~": "vim::ChangeCase",
"*": ["vim::MoveToNext", { "partial_word": true }],
"#": ["vim::MoveToPrev", { "partial_word": true }],
"#": ["vim::MoveToPrevious", { "partial_word": true }],
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"g ctrl-a": ["vim::Increment", { "step": true }],
@@ -448,7 +448,10 @@
"d": "vim::CurrentLine",
"s": "vim::PushDeleteSurrounds",
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"p": "git::Restore" // "d p"
"shift-o": "git::ToggleStaged",
"p": "git::Restore", // "d p"
"u": "git::StageAndNext", // "d u"
"shift-u": "git::UnstageAndNext" // "d shift-u"
}
},
{
@@ -620,8 +623,8 @@
"ctrl-w =": "vim::ResetPaneSizes",
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
"ctrl-w g shift-t": "pane::ActivatePreviousItem",
"ctrl-w ctrl-g shift-t": "pane::ActivatePreviousItem",
"ctrl-w w": "workspace::ActivateNextPane",
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
"ctrl-w p": "workspace::ActivatePreviousPane",
@@ -664,7 +667,7 @@
"escape": "project_panel::ToggleFocus",
"h": "project_panel::CollapseSelectedEntry",
"j": "menu::SelectNext",
"k": "menu::SelectPrev",
"k": "menu::SelectPrevious",
"l": "project_panel::ExpandSelectedEntry",
"o": "project_panel::OpenPermanent",
"shift-d": "project_panel::Delete",
@@ -690,7 +693,7 @@
"context": "OutlinePanel && not_editing",
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",
"k": "menu::SelectPrevious",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst"
}
@@ -699,7 +702,7 @@
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"k": "menu::SelectPrev",
"k": "menu::SelectPrevious",
"j": "menu::SelectNext",
"g g": "menu::SelectFirst",
"shift-g": "menu::SelectLast",

View File

@@ -648,11 +648,19 @@
// Show git status colors in the editor tabs.
"git_status": false,
// Position of the close button on the editor tabs.
// One of: ["right", "left", "hidden"]
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,
// Whether to always show the close button on tabs.
"always_show_close_button": false,
// Controls the appearance behavior of the tab's close button.
//
// 1. Show it just upon hovering the tab. (default)
// "hover"
// 2. Show it persistently.
// "always"
// 3. Never show it, even if hovering it.
// "hidden"
"show_close_button": "hover",
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
@@ -829,7 +837,15 @@
//
// The minimum column number to show the inline blame information at
// "min_column": 0
}
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
// 1. Show unstaged hunks with a transparent background (default):
// "hunk_style": "transparent"
// 2. Show unstaged hunks with a pattern background:
// "hunk_style": "pattern"
"hunk_style": "transparent"
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.
@@ -843,15 +859,7 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.

View File

@@ -51,6 +51,7 @@ parking_lot.workspace = true
paths.workspace = true
project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true

View File

@@ -19,7 +19,7 @@ use gpui::{actions, App, Global, UpdateGlobal};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::Deserialize;
use settings::{Settings, SettingsStore};

View File

@@ -24,7 +24,8 @@ use language_model::{
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;

View File

@@ -35,11 +35,11 @@ use language_model::{
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
};
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
use language_model_selector::inline_language_model_selector;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use rope::Rope;
use settings::{update_settings_file, Settings, SettingsStore};
use smol::future::FutureExt;
@@ -1425,7 +1425,6 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
editor: Entity<Editor>,
language_model_selector: Entity<LanguageModelSelector>,
edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
@@ -1439,6 +1438,7 @@ struct PromptEditor {
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakEntity<Workspace>>,
show_rate_limit_notice: bool,
fs: Arc<dyn Fs>,
}
#[derive(Copy, Clone)]
@@ -1567,6 +1567,7 @@ impl Render for PromptEditor {
]
}
});
let fs_clone = self.fs.clone();
h_flex()
.key_context("PromptEditor")
@@ -1589,10 +1590,13 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(
InlineLanguageModelSelector::new(self.language_model_selector.clone())
.render(window, cx),
)
.child(inline_language_model_selector(move |model, cx| {
update_settings_file::<AssistantSettings>(
fs_clone.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
@@ -1705,21 +1709,8 @@ impl PromptEditor {
let mut this = Self {
id,
fs,
editor: prompt_editor,
language_model_selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
edited_since_done: false,
gutter_dimensions,
prompt_history,

View File

@@ -19,8 +19,8 @@ use language_model::{
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
};
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
use prompt_library::PromptBuilder;
use language_model_selector::inline_language_model_selector;
use prompt_store::PromptBuilder;
use settings::{update_settings_file, Settings};
use std::{
cmp,
@@ -487,9 +487,9 @@ enum PromptEditorEvent {
struct PromptEditor {
id: TerminalInlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8,
editor: Entity<Editor>,
language_model_selector: Entity<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -506,7 +506,7 @@ struct PromptEditor {
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let buttons = match status {
CodegenStatus::Idle => {
@@ -624,6 +624,8 @@ impl Render for PromptEditor {
}
};
let fs_clone = self.fs.clone();
h_flex()
.bg(cx.theme().colors().editor_background)
.border_y_1()
@@ -641,10 +643,13 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(
InlineLanguageModelSelector::new(self.language_model_selector.clone())
.render(window, cx),
)
.child(inline_language_model_selector(move |model, cx| {
update_settings_file::<AssistantSettings>(
fs_clone.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
@@ -722,22 +727,9 @@ impl PromptEditor {
let mut this = Self {
id,
fs,
height_in_lines: 1,
editor: prompt_editor,
language_model_selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
edited_since_done: false,
prompt_history,
prompt_history_ix: None,

View File

@@ -56,6 +56,7 @@ paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
serde.workspace = true

View File

@@ -1,18 +1,19 @@
use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use assistant_tool::{ToolRegistry, ToolWorkingSet};
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length,
ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement,
UnderlineStyle, WeakEntity,
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
};
use language::LanguageRegistry;
use language::{Buffer, Language, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure};
use ui::{prelude::*, Disclosure, KeyBinding};
use util::ResultExt as _;
use workspace::Workspace;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
@@ -26,14 +27,21 @@ 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>,
lua_language: Option<Arc<Language>>, // Used for syntax highlighting in the Lua script tool
_subscriptions: Vec<Subscription>,
}
struct EditMessageState {
editor: Entity<Editor>,
}
impl ActiveThread {
pub fn new(
thread: Entity<Thread>,
@@ -55,20 +63,36 @@ 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, _: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, cx))
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, window, cx))
.unwrap()
}
}),
editing_message: None,
last_error: None,
lua_language: None,
_subscriptions: subscriptions,
};
// Initialize the Lua language in the background, for syntax highlighting.
let language_registry = this.language_registry.clone();
cx.spawn(|this, mut cx| async move {
if let Ok(lua_language) = language_registry.language_for_name("Lua").await {
this.update(&mut cx, |this, _| {
this.lua_language = Some(lua_language);
})?;
}
Ok::<_, anyhow::Error>(())
})
.detach();
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), window, cx);
}
@@ -117,6 +141,44 @@ 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);
@@ -134,6 +196,8 @@ 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())),
@@ -180,20 +244,15 @@ impl ActiveThread {
..Default::default()
};
let markdown = cx.new(|cx| {
cx.new(|cx| {
Markdown::new(
text.into(),
text,
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(
@@ -208,11 +267,7 @@ impl ActiveThread {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
self.thread_store
.update(cx, |thread_store, cx| {
thread_store.save_thread(&self.thread, cx)
})
.detach_and_log_err(cx);
self.save_thread(cx);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
@@ -231,18 +286,31 @@ impl ActiveThread {
self.push_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::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.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageDeleted(message_id) => {
self.deleted_message(message_id);
self.save_thread(cx);
cx.notify();
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
.thread
.read(cx)
let thread = self.thread.read(cx);
let thread_id = thread.id().0.clone();
let pending_tool_uses = thread
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
@@ -251,7 +319,13 @@ impl ActiveThread {
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
let task = tool.run(
tool_use.input,
thread_id.clone(),
self.workspace.clone(),
window,
cx,
);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(tool_use.id.clone(), task, cx);
@@ -270,8 +344,15 @@ impl ActiveThread {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(model) = model_registry.active_model() {
self.thread.update(cx, |thread, cx| {
// Insert an empty user message to contain the tool results.
thread.insert_user_message("", Vec::new(), cx);
// Insert a user message to contain the tool results.
thread.insert_user_message(
// TODO: Sending up a user message without any content results in the model sending back
// responses that also don't have any content. We currently don't handle this case well,
// so for now we provide some text to keep the model on track.
"Here are the tool results.",
Vec::new(),
cx,
);
thread.send_to_model(model, RequestKind::Chat, true, cx);
});
}
@@ -280,7 +361,133 @@ impl ActiveThread {
}
}
fn render_message(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
/// 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 {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -295,15 +502,32 @@ impl ActiveThread {
let colors = cx.theme().colors();
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User
&& message.text.is_empty()
&& self.thread.read(cx).message_has_tool_results(message_id)
{
if message.role == Role::User && self.thread.read(cx).message_has_tool_results(message_id) {
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(div().p_2p5().text_ui(cx).child(markdown.clone()))
.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())
},
)
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(
@@ -333,7 +557,8 @@ impl ActiveThread {
.child(
h_flex()
.py_1()
.px_2()
.pl_2()
.pr_1()
.bg(colors.editor_foreground.opacity(0.05))
.border_b_1()
.border_color(colors.border)
@@ -352,6 +577,71 @@ 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),
@@ -384,6 +674,7 @@ impl ActiveThread {
}
fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
let tool = ToolRegistry::global(cx).tool(&tool_use.name);
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
@@ -449,11 +740,17 @@ impl ActiveThread {
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Input:"))
.child(Label::new(
serde_json::to_string_pretty(&tool_use.input)
.unwrap_or_default(),
)),
.bg(cx.theme().colors().editor_background)
.child(match tool.clone() {
Some(tool) => tool.render_input(
tool_use.input,
self.lua_language.clone(),
cx,
),
None => {
assistant_tool::default_render_input(tool_use.input)
}
}),
)
.map(|parent| match tool_use.status {
ToolUseStatus::Finished(output) => parent.child(
@@ -461,16 +758,17 @@ impl ActiveThread {
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Result:"))
.child(Label::new(output)),
.bg(cx.theme().colors().editor_background)
.child(match tool {
Some(tool) => tool.render_output(output, cx),
None => assistant_tool::default_render_output(output),
}),
),
ToolUseStatus::Error(err) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Error:"))
.child(Label::new(err)),
v_flex().gap_0p5().py_1().px_2p5().child(match tool {
Some(tool) => tool.render_error(err, cx),
None => assistant_tool::default_render_output(err),
}),
),
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
}),

View File

@@ -27,7 +27,7 @@ use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, App};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use settings::Settings as _;
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};

View File

@@ -1,46 +1,50 @@
use assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
use gpui::FocusHandle;
use language_model_selector::{assistant_language_model_selector, LanguageModelSelector};
use settings::update_settings_file;
use std::sync::Arc;
use ui::prelude::*;
use ui::{prelude::*, PopoverMenuHandle};
pub struct AssistantModelSelector {
pub selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
_window: &mut Window,
_cx: &mut App,
) -> Self {
Self {
selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
window,
cx,
)
}),
fs,
focus_handle,
menu_handle: PopoverMenuHandle::default(),
}
}
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
self.menu_handle.toggle(window, cx);
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone())
.render(window, cx)
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let fs_clone = self.fs.clone();
assistant_language_model_selector(
self.focus_handle.clone(),
Some(self.menu_handle.clone()),
cx,
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs_clone.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
)
}
}

View File

@@ -15,12 +15,14 @@ use editor::Editor;
use fs::Fs;
use gpui::{
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
WeakEntity,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
use project::Project;
use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder;
use settings::{update_settings_file, Settings};
use time::UtcOffset;
use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
@@ -608,7 +610,7 @@ impl AssistantPanel {
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).text_ellipsis()),
.child(Label::new(title).truncate()),
)
.child(
h_flex()
@@ -992,12 +994,21 @@ 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("AssistantPanel2")
.key_context(self.key_context())
.justify_between()
.size_full()
.on_action(cx.listener(Self::cancel))

View File

@@ -14,7 +14,7 @@ use language_model::{
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use rope::Rope;
use smol::future::FutureExt;
use std::{

View File

@@ -28,7 +28,7 @@ use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};

View File

@@ -20,6 +20,7 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;
@@ -102,11 +103,9 @@ impl<T: 'static> Render for PromptEditor<T> {
.items_start()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(|this, action, window, cx| {
let selector = this.model_selector.read(cx).selector.clone();
selector.update(cx, |selector, cx| {
selector.toggle_model_selector(action, window, cx);
})
.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::confirm))
.on_action(cx.listener(Self::cancel))

View File

@@ -8,6 +8,7 @@ use gpui::{
TextStyle, WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
use rope::Point;
use settings::Settings;
use std::time::Duration;
@@ -297,11 +298,9 @@ impl Render for MessageEditor {
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, action, window, cx| {
let selector = this.model_selector.read(cx).selector.clone();
selector.update(cx, |this, cx| {
this.toggle_model_selector(action, window, cx);
})
.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))

View File

@@ -16,7 +16,7 @@ use language_model::{
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use std::sync::Arc;
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::TerminalView;

View File

@@ -8,8 +8,9 @@ use futures::StreamExt as _;
use gpui::{App, Context, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolUseId,
MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, Role, StopReason,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason,
};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
@@ -27,7 +28,7 @@ pub enum RequestKind {
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ThreadId(Arc<str>);
pub struct ThreadId(pub Arc<str>);
impl ThreadId {
pub fn new() -> Self {
@@ -88,7 +89,7 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use: ToolUseState::default(),
tool_use: ToolUseState::new(),
}
}
@@ -98,7 +99,14 @@ impl Thread {
tools: Arc<ToolWorkingSet>,
_cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(saved.messages.len());
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);
Self {
id,
@@ -120,7 +128,7 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use: ToolUseState::default(),
tool_use,
}
}
@@ -189,6 +197,10 @@ 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)
}
@@ -223,6 +235,34 @@ 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.
@@ -561,6 +601,8 @@ pub enum ThreadEvent {
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
MessageEdited(MessageId),
MessageDeleted(MessageId),
SummaryChanged,
UsePendingTools,
ToolFinished {

View File

@@ -33,9 +33,9 @@ impl ThreadHistory {
}
}
pub fn select_prev(
pub fn select_previous(
&mut self,
_: &menu::SelectPrev,
_: &menu::SelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -166,7 +166,7 @@ impl Render for ThreadHistory {
.overflow_y_scroll()
.size_full()
.p_1()
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
@@ -260,7 +260,7 @@ impl RenderOnce for PastThread {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()
@@ -356,7 +356,7 @@ impl RenderOnce for PastContext {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()

View File

@@ -12,9 +12,9 @@ use futures::FutureExt as _;
use gpui::{
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
};
use heed::types::SerdeBincode;
use heed::types::{SerdeBincode, SerdeJson};
use heed::Database;
use language_model::Role;
use language_model::{LanguageModelToolUseId, Role};
use project::Project;
use serde::{Deserialize, Serialize};
use util::ResultExt as _;
@@ -113,6 +113,24 @@ 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(),
};
@@ -239,11 +257,29 @@ pub struct SavedThread {
pub messages: Vec<SavedMessage>,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, 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(
@@ -255,7 +291,7 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerdeBincode<SavedThread>>,
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
}
impl ThreadsDatabase {
@@ -270,7 +306,7 @@ impl ThreadsDatabase {
let database_future = executor
.spawn({
let executor = executor.clone();
let database_path = paths::support_dir().join("threads/threads-db.0.mdb");
let database_path = paths::support_dir().join("threads/threads-db.1.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))

View File

@@ -7,10 +7,11 @@ use futures::FutureExt as _;
use gpui::{SharedString, Task};
use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent,
LanguageModelToolUseId, MessageContent, Role,
};
use crate::thread::MessageId;
use crate::thread_store::SavedMessage;
#[derive(Debug)]
pub struct ToolUse {
@@ -28,7 +29,6 @@ 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,6 +37,65 @@ 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()
}
@@ -84,6 +143,17 @@ 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

@@ -37,7 +37,7 @@ parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true

View File

@@ -27,7 +27,7 @@ use language_model::{
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
use project::Project;
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{

View File

@@ -20,7 +20,7 @@ use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Rol
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::Project;
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
@@ -671,7 +671,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_library::init);
cx.update(prompt_store::init);
let mut settings_store = cx.update(SettingsStore::test);
cx.update(|cx| {
settings_store

View File

@@ -37,7 +37,9 @@ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
use language_model_selector::{
assistant_language_model_selector, LanguageModelSelector, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::lsp_store::LocalLspAdapterDelegate;
@@ -52,7 +54,7 @@ use ui::{
Tooltip,
};
use util::{maybe, ResultExt};
use workspace::searchable::SearchableItemHandle;
use workspace::searchable::{Direction, SearchableItemHandle};
use workspace::{
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
@@ -195,7 +197,7 @@ pub struct ContextEditor {
// the file is opened. In order to keep the worktree alive for the duration of the
// context editor, we keep a reference here.
dragged_file_worktrees: Vec<Entity<Worktree>>,
language_model_selector: Entity<LanguageModelSelector>,
language_model_selector: PopoverMenuHandle<LanguageModelSelector>,
}
pub const DEFAULT_TAB_TITLE: &str = "New Chat";
@@ -249,21 +251,6 @@ impl ContextEditor {
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
];
let fs_clone = fs.clone();
let language_model_selector = cx.new(|cx| {
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs_clone.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
window,
cx,
)
});
let sections = context.read(cx).slash_command_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone();
@@ -288,7 +275,7 @@ impl ContextEditor {
show_accept_terms: false,
slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(),
language_model_selector,
language_model_selector: PopoverMenuHandle::default(),
};
this.update_message_headers(cx);
this.update_image_blocks(cx);
@@ -2831,6 +2818,7 @@ impl Render for ContextEditor {
} else {
None
};
let fs_clone = self.fs.clone();
let language_model_selector = self.language_model_selector.clone();
v_flex()
@@ -2845,10 +2833,8 @@ impl Render for ContextEditor {
.on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.on_action(move |action, window, cx| {
language_model_selector.update(cx, |this, cx| {
this.toggle_model_selector(action, window, cx);
})
.on_action(move |_: &ToggleModelSelector, window, cx| {
language_model_selector.toggle(window, cx);
})
.size_full()
.children(self.render_notice(cx))
@@ -2887,14 +2873,18 @@ impl Render for ContextEditor {
.gap_1()
.child(self.render_inject_context_menu(cx))
.child(ui::Divider::vertical())
.child(div().pl_0p5().child({
let focus_handle = self.editor().focus_handle(cx).clone();
AssistantLanguageModelSelector::new(
focus_handle,
self.language_model_selector.clone(),
)
.render(window, cx)
})),
.child(div().pl_0p5().child(assistant_language_model_selector(
self.editor().focus_handle(cx),
Some(self.language_model_selector.clone()),
cx,
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs_clone.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
))),
)
.child(
h_flex()
@@ -3070,12 +3060,13 @@ 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(matches, window, cx)
editor.active_match_index(direction, matches, window, cx)
})
}
}

View File

@@ -16,7 +16,7 @@ use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task,
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use regex::Regex;
use rpc::AnyProtoClient;
use std::sync::LazyLock;

View File

@@ -207,24 +207,31 @@ impl PickerDelegate for SlashCommandDelegate {
.child(
h_flex()
.gap_1p5()
.child(Icon::new(info.icon).size(IconSize::XSmall))
.child(div().font_buffer(cx).child({
.child(
Icon::new(info.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child({
let mut label = format!("{}", info.name);
if let Some(args) = info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).single_line().size(LabelSize::Small)
}))
Label::new(label)
.single_line()
.size(LabelSize::Small)
.buffer_font(cx)
})
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args)
.single_line()
.size(LabelSize::Small)
.color(Color::Muted),
.color(Color::Muted)
.buffer_font(cx),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
@@ -236,7 +243,7 @@ impl PickerDelegate for SlashCommandDelegate {
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.text_ellipsis(),
.truncate(),
),
),
),
@@ -294,10 +301,9 @@ where
.gap_1p5()
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("create-your-command")
.size(LabelSize::Small),
),
Label::new("create-your-command")
.size(LabelSize::Small)
.buffer_font(cx),
),
)
.child(
@@ -341,7 +347,7 @@ where
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
y: px(-2.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}

View File

@@ -32,7 +32,7 @@ language.workspace = true
language_model.workspace = true
log.workspace = true
project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
rope.workspace = true
schemars.workspace = true
semantic_index.workspace = true

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use prompt_library::PromptStore;
use prompt_store::PromptStore;
use std::{
fmt::Write,
sync::{atomic::AtomicBool, Arc},

View File

@@ -13,7 +13,7 @@ use feature_flags::FeatureFlag;
use gpui::{App, Task, WeakEntity};
use language::{Anchor, CodeLabel, LspAdapterDelegate};
use language_model::{LanguageModelRegistry, LanguageModelTool};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use semantic_index::SemanticDb;
use serde::Deserialize;

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use prompt_library::PromptStore;
use prompt_store::PromptStore;
use std::sync::{atomic::AtomicBool, Arc};
use ui::prelude::*;
use workspace::Workspace;

View File

@@ -16,7 +16,9 @@ anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
language.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace.workspace = true
ui.workspace = true

View File

@@ -4,7 +4,16 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::IntoElement;
use gpui::{App, Task, WeakEntity, Window};
use language::Language;
use ui::div;
use ui::Label;
use ui::LabelCommon;
use ui::LabelSize;
use ui::ParentElement;
use ui::SharedString;
use workspace::Workspace;
pub use crate::tool_registry::*;
@@ -31,8 +40,52 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
thread_id: Arc<str>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<String>>;
/// Renders the tool's input when the user expands it.
fn render_input(
self: Arc<Self>,
input: serde_json::Value,
_lua_language: Option<Arc<Language>>,
_cx: &mut App,
) -> AnyElement {
default_render_input(input)
}
/// Renders the tool's output when the user expands it.
fn render_output(self: Arc<Self>, output: SharedString, _cx: &mut App) -> AnyElement {
default_render_output(output)
}
/// Renders the tool's error message when the user expands it.
fn render_error(self: Arc<Self>, err: SharedString, _cx: &mut App) -> AnyElement {
default_render_error(err)
}
}
pub fn default_render_input(input: serde_json::Value) -> AnyElement {
div()
.child(Label::new("Input:").size(LabelSize::Small))
.child(Label::new(
serde_json::to_string_pretty(&input).unwrap_or_default(),
))
.into_any_element()
}
pub fn default_render_output(output: SharedString) -> AnyElement {
div()
.child(Label::new("Result:").size(LabelSize::Small))
.child(Label::new(output))
.into_any_element()
}
pub fn default_render_error(err: SharedString) -> AnyElement {
div()
.child(Label::new("Error:").size(LabelSize::Small))
.child(Label::new(err))
.into_any_element()
}

View File

@@ -1,11 +1,10 @@
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use chrono::{Local, Utc};
use gpui::{App, Task, WeakEntity, Window};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -41,6 +40,7 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_thread_id: Arc<str>,
_workspace: WeakEntity<workspace::Workspace>,
_window: &mut Window,
_cx: &mut App,

View File

@@ -141,19 +141,20 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
cx,
move |cx| {
let workspace_handle = cx.entity().downgrade();
cx.new(|_cx| {
MessageNotification::new(format!("Updated to {app_name} {}", version))
.primary_message("View Release Notes")
.primary_on_click(move |window, cx| {
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::view_release_notes_locally(
workspace, window, cx,
);
})
}
cx.emit(DismissEvent);
})
cx.new(|cx| {
MessageNotification::new(
format!("Updated to {app_name} {}", version),
cx,
)
.primary_message("View Release Notes")
.primary_on_click(move |window, cx| {
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::view_release_notes_locally(workspace, window, cx);
})
}
cx.emit(DismissEvent);
})
})
},
);

View File

@@ -82,7 +82,7 @@ impl Render for Breadcrumbs {
text_style.color = Color::Muted.color(cx);
StyledText::new(segment.text.replace('\n', ""))
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
.with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
.into_any()
});
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {

View File

@@ -16,6 +16,7 @@ test-support = []
[dependencies]
anyhow.workspace = true
clock.workspace = true
futures.workspace = true
git2.workspace = true
gpui.workspace = true

View File

@@ -1,11 +1,12 @@
use futures::{channel::oneshot, future::OptionFuture};
use futures::channel::oneshot;
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use sum_tree::SumTree;
use sum_tree::{SumTree, TreeMap};
use text::ToOffset as _;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use util::ResultExt;
@@ -20,13 +21,14 @@ pub struct BufferDiff {
pub struct BufferDiffSnapshot {
inner: BufferDiffInner,
secondary_diff: Option<Box<BufferDiffSnapshot>>,
pub is_single_insertion: bool,
}
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
base_text: Option<language::BufferSnapshot>,
pending_hunks: TreeMap<usize, PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -47,23 +49,15 @@ pub enum DiffHunkSecondaryStatus {
HasSecondaryHunk,
OverlapsWithSecondaryHunk,
None,
}
impl DiffHunkSecondaryStatus {
pub fn is_secondary(&self) -> bool {
match self {
DiffHunkSecondaryStatus::HasSecondaryHunk => true,
DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => true,
DiffHunkSecondaryStatus::None => false,
}
}
SecondaryHunkAdditionPending,
SecondaryHunkRemovalPending,
}
/// A diff hunk resolved to rows in the buffer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffHunk {
/// The buffer range, expressed in terms of rows.
pub row_range: Range<u32>,
/// The buffer range as points.
pub range: Range<Point>,
/// The range in the buffer to which this hunk corresponds.
pub buffer_range: Range<Anchor>,
/// The range in the buffer's diff base text to which this hunk corresponds.
@@ -78,6 +72,17 @@ struct InternalDiffHunk {
diff_base_byte_range: Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk {
buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus,
}
#[derive(Debug, Default, Clone)]
pub struct DiffHunkSummary {
buffer_range: Range<Anchor>,
}
impl sum_tree::Item for InternalDiffHunk {
type Summary = DiffHunkSummary;
@@ -88,11 +93,6 @@ impl sum_tree::Item for InternalDiffHunk {
}
}
#[derive(Debug, Default, Clone)]
pub struct DiffHunkSummary {
buffer_range: Range<Anchor>,
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
@@ -159,131 +159,166 @@ impl BufferDiffSnapshot {
self.inner.hunks_intersecting_range_rev(range, buffer)
}
pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
self.inner.base_text.as_ref()
pub fn base_text(&self) -> &language::BufferSnapshot {
&self.inner.base_text
}
pub fn base_texts_eq(&self, other: &Self) -> bool {
match (other.base_text(), self.base_text()) {
(None, None) => true,
(None, Some(_)) => false,
(Some(_), None) => false,
(Some(old), Some(new)) => {
let (old_id, old_empty) = (old.remote_id(), old.is_empty());
let (new_id, new_empty) = (new.remote_id(), new.is_empty());
new_id == old_id || (new_empty && old_empty)
}
if self.inner.base_text_exists != other.inner.base_text_exists {
return false;
}
let left = &self.inner.base_text;
let right = &other.inner.base_text;
let (old_id, old_empty) = (left.remote_id(), left.is_empty());
let (new_id, new_empty) = (right.remote_id(), right.is_empty());
new_id == old_id || (new_empty && old_empty)
}
}
pub fn new_secondary_text_for_stage_or_unstage(
&self,
impl BufferDiffInner {
fn stage_or_unstage_hunks(
&mut self,
unstaged_diff: &Self,
stage: bool,
hunks: impl Iterator<Item = (Range<Anchor>, Range<usize>)>,
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
cx: &mut App,
) -> Option<Rope> {
let secondary_diff = self.secondary_diff()?;
let head_text = self.base_text().map(|text| text.as_rope().clone());
let index_text = secondary_diff
.base_text()
.map(|text| text.as_rope().clone());
file_exists: bool,
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
let index_text = unstaged_diff
.base_text_exists
.then(|| unstaged_diff.base_text.as_rope().clone());
// If the file doesn't exist in either HEAD or the index, then the
// entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) => (index_text, head_text),
// file is deleted in both index and head
(None, None) => return None,
// file is deleted in index
(None, Some(head_text)) => {
return if stage {
Some(buffer.as_rope().clone())
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(_, head_text @ _) => {
if stage {
log::debug!("stage all");
return (
file_exists.then(|| buffer.as_rope().clone()),
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
},
)],
);
} else {
Some(head_text)
}
}
// file exists in the index, but is deleted in head
(Some(_), None) => {
return if stage {
Some(buffer.as_rope().clone())
} else {
None
log::debug!("unstage all");
return (
head_text,
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
},
)],
);
}
}
};
let mut secondary_cursor = secondary_diff.inner.hunks.cursor::<DiffHunkSummary>(buffer);
secondary_cursor.next(buffer);
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut edits = Vec::new();
let mut prev_secondary_hunk_buffer_offset = 0;
let mut prev_secondary_hunk_base_text_offset = 0;
for (buffer_range, diff_base_byte_range) in hunks {
let skipped_hunks = secondary_cursor.slice(&buffer_range.start, Bias::Left, buffer);
let mut pending_hunks = Vec::new();
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
for DiffHunk {
buffer_range,
diff_base_byte_range,
secondary_status,
..
} in hunks.iter().cloned()
{
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
continue;
}
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_secondary_hunk_buffer_offset =
prev_unstaged_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset =
secondary_hunk.buffer_range.end.to_offset(buffer);
}
let mut buffer_offset_range = buffer_range.to_offset(buffer);
let start_overshoot = buffer_offset_range.start - prev_secondary_hunk_buffer_offset;
let mut secondary_base_text_start =
prev_secondary_hunk_base_text_offset + start_overshoot;
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_offset;
let mut index_start = prev_unstaged_hunk_base_text_offset + start_overshoot;
while let Some(secondary_hunk) = secondary_cursor.item().filter(|item| {
while let Some(unstaged_hunk) = unstaged_hunk_cursor.item().filter(|item| {
item.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
let secondary_hunk_offset_range = secondary_hunk.buffer_range.to_offset(buffer);
prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_secondary_hunk_buffer_offset = secondary_hunk_offset_range.end;
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
prev_unstaged_hunk_base_text_offset = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset = unstaged_hunk_offset_range.end;
secondary_base_text_start =
secondary_base_text_start.min(secondary_hunk.diff_base_byte_range.start);
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(secondary_hunk_offset_range.start);
.min(unstaged_hunk_offset_range.start);
secondary_cursor.next(buffer);
unstaged_hunk_cursor.next(buffer);
}
let end_overshoot = buffer_offset_range
.end
.saturating_sub(prev_secondary_hunk_buffer_offset);
let secondary_base_text_end = prev_secondary_hunk_base_text_offset + end_overshoot;
.saturating_sub(prev_unstaged_hunk_buffer_offset);
let index_end = prev_unstaged_hunk_base_text_offset + end_overshoot;
let secondary_base_text_range = secondary_base_text_start..secondary_base_text_end;
let index_range = index_start..index_end;
buffer_offset_range.end = buffer_offset_range
.end
.max(prev_secondary_hunk_buffer_offset);
.max(prev_unstaged_hunk_buffer_offset);
let replacement_text = if stage {
log::debug!("staging");
log::debug!("stage hunk {:?}", buffer_offset_range);
buffer
.text_for_range(buffer_offset_range)
.collect::<String>()
} else {
log::debug!("unstaging");
log::debug!("unstage hunk {:?}", buffer_offset_range);
head_text
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
};
edits.push((secondary_base_text_range, replacement_text));
pending_hunks.push((
diff_base_byte_range.start,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
));
edits.push((index_range, replacement_text));
}
let buffer = cx.new(|cx| {
language::Buffer::local_normalized(index_text, text::LineEnding::default(), cx)
});
let new_text = buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
buffer.as_rope().clone()
});
Some(new_text)
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
new_index_text.push(&replacement_text);
}
new_index_text.append(index_cursor.suffix());
(Some(new_index_text), pending_hunks)
}
}
impl BufferDiffInner {
fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
@@ -318,12 +353,16 @@ impl BufferDiffInner {
]
});
let mut secondary_cursor = secondary.as_ref().map(|diff| {
let mut cursor = diff.hunks.cursor::<DiffHunkSummary>(buffer);
let mut secondary_cursor = None;
let mut pending_hunks = TreeMap::default();
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
cursor
});
secondary_cursor = Some(cursor);
pending_hunks = secondary.pending_hunks.clone();
}
let max_point = buffer.max_point();
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || loop {
let (start_point, (start_anchor, start_base)) = summaries.next()?;
@@ -333,14 +372,26 @@ impl BufferDiffInner {
continue;
}
if end_point.column > 0 {
if end_point.column > 0 && end_point < max_point {
end_point.row += 1;
end_point.column = 0;
end_anchor = buffer.anchor_before(end_point);
}
let mut secondary_status = DiffHunkSecondaryStatus::None;
if let Some(secondary_cursor) = secondary_cursor.as_mut() {
let mut has_pending = false;
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
if start_anchor
.cmp(&secondary_cursor.start().buffer_range.start, buffer)
.is_gt()
@@ -354,18 +405,19 @@ impl BufferDiffInner {
secondary_range.end.row += 1;
secondary_range.end.column = 0;
}
if secondary_range == (start_point..end_point) {
if secondary_range.is_empty() && secondary_hunk.diff_base_byte_range.is_empty()
{
// ignore
} else if secondary_range == (start_point..end_point) {
secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
} else if secondary_range.start <= end_point {
secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
}
}
} else {
log::debug!("no secondary cursor!!");
}
return Some(DiffHunk {
row_range: start_point.row..end_point.row,
range: start_point..end_point,
diff_base_byte_range: start_base..end_base,
buffer_range: start_anchor..end_anchor,
secondary_status,
@@ -391,14 +443,9 @@ impl BufferDiffInner {
let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
} else {
range.end.row
};
Some(DiffHunk {
row_range: range.start.row..end_row,
range,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
@@ -518,6 +565,14 @@ fn compute_hunks(
tree.push(hunk, &buffer);
}
}
} else {
tree.push(
InternalDiffHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..0,
},
&buffer,
);
}
tree
@@ -631,95 +686,71 @@ impl BufferDiff {
fn build(
buffer: text::BufferSnapshot,
diff_base: Option<Arc<String>>,
base_text: Option<Arc<String>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut App,
) -> impl Future<Output = BufferDiffInner> {
let diff_base =
diff_base.map(|diff_base| (diff_base.clone(), Rope::from(diff_base.as_str())));
let base_text_snapshot = diff_base.as_ref().map(|(_, diff_base)| {
language::Buffer::build_snapshot(
diff_base.clone(),
let base_text_pair;
let base_text_exists;
let base_text_snapshot;
if let Some(text) = &base_text {
let base_text_rope = Rope::from(text.as_str());
base_text_pair = Some((text.clone(), base_text_rope.clone()));
let snapshot = language::Buffer::build_snapshot(
base_text_rope,
language.clone(),
language_registry.clone(),
cx,
)
});
let base_text_snapshot = cx.background_spawn(OptionFuture::from(base_text_snapshot));
);
base_text_snapshot = cx.background_spawn(snapshot);
base_text_exists = true;
} else {
base_text_pair = None;
base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx));
base_text_exists = false;
};
let hunks = cx.background_spawn({
let buffer = buffer.clone();
async move { compute_hunks(diff_base, buffer) }
async move { compute_hunks(base_text_pair, buffer) }
});
async move {
let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
BufferDiffInner { base_text, hunks }
BufferDiffInner {
base_text,
hunks,
base_text_exists,
pending_hunks: TreeMap::default(),
}
}
}
fn build_with_base_buffer(
buffer: text::BufferSnapshot,
diff_base: Option<Arc<String>>,
diff_base_buffer: Option<language::BufferSnapshot>,
base_text: Option<Arc<String>>,
base_text_snapshot: language::BufferSnapshot,
cx: &App,
) -> impl Future<Output = BufferDiffInner> {
let diff_base = diff_base.clone().zip(
diff_base_buffer
.clone()
.map(|buffer| buffer.as_rope().clone()),
);
let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
cx.background_spawn(async move {
BufferDiffInner {
hunks: compute_hunks(diff_base, buffer),
base_text: diff_base_buffer,
base_text: base_text_snapshot,
hunks: compute_hunks(base_text_pair, buffer),
pending_hunks: TreeMap::default(),
base_text_exists,
}
})
}
fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner {
fn build_empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffInner {
BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer),
base_text: None,
}
}
pub fn build_with_single_insertion(
insertion_present_in_secondary_diff: bool,
buffer: language::BufferSnapshot,
cx: &mut App,
) -> BufferDiffSnapshot {
let base_text = language::Buffer::build_empty_snapshot(cx);
let hunks = SumTree::from_item(
InternalDiffHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..0,
},
&base_text,
);
BufferDiffSnapshot {
inner: BufferDiffInner {
hunks: hunks.clone(),
base_text: Some(base_text.clone()),
},
secondary_diff: Some(Box::new(BufferDiffSnapshot {
inner: BufferDiffInner {
hunks: if insertion_present_in_secondary_diff {
hunks
} else {
SumTree::new(&buffer.text)
},
base_text: Some(if insertion_present_in_secondary_diff {
base_text
} else {
buffer
}),
},
secondary_diff: None,
is_single_insertion: true,
})),
is_single_insertion: true,
pending_hunks: TreeMap::default(),
base_text_exists: false,
}
}
@@ -728,7 +759,38 @@ impl BufferDiff {
}
pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
Some(self.secondary_diff.as_ref()?.clone())
self.secondary_diff.clone()
}
pub fn stage_or_unstage_hunks(
&mut self,
stage: bool,
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
buffer,
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
for (offset, pending_hunk) in pending_hunks {
diff.inner.pending_hunks.insert(offset, pending_hunk);
}
});
}
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
let changed_range = first.buffer_range.start..last.buffer_range.end;
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
});
}
new_index_text
}
pub fn range_to_hunk_range(
@@ -777,7 +839,7 @@ impl BufferDiff {
Self::build_with_base_buffer(
buffer.clone(),
base_text,
this.base_text().cloned(),
this.base_text().clone(),
cx,
)
})?
@@ -799,22 +861,33 @@ impl BufferDiff {
fn set_state(
&mut self,
inner: BufferDiffInner,
new_state: BufferDiffInner,
buffer: &text::BufferSnapshot,
) -> Option<Range<Anchor>> {
let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) {
(None, None) => None,
(Some(old), Some(new)) if old.remote_id() == new.remote_id() => {
inner.compare(&self.inner, buffer)
}
_ => Some(text::Anchor::MIN..text::Anchor::MAX),
};
self.inner = inner;
let (base_text_changed, changed_range) =
match (self.inner.base_text_exists, new_state.base_text_exists) {
(false, false) => (true, None),
(true, true)
if self.inner.base_text.remote_id() == new_state.base_text.remote_id() =>
{
(false, new_state.compare(&self.inner, buffer))
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
};
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
self.inner = new_state;
if !base_text_changed {
self.inner.pending_hunks = pending_hunks;
}
changed_range
}
pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
self.inner.base_text.as_ref()
pub fn base_text(&self) -> &language::BufferSnapshot {
&self.inner.base_text
}
pub fn base_text_exists(&self) -> bool {
self.inner.base_text_exists
}
pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
@@ -824,7 +897,6 @@ impl BufferDiff {
.secondary_diff
.as_ref()
.map(|diff| Box::new(diff.read(cx).snapshot(cx))),
is_single_insertion: false,
}
}
@@ -901,15 +973,16 @@ impl BufferDiff {
rx
}
#[cfg(any(test, feature = "test-support"))]
pub fn base_text_string(&self) -> Option<String> {
self.inner.base_text.as_ref().map(|buffer| buffer.text())
self.inner
.base_text_exists
.then(|| self.inner.base_text.text())
}
pub fn new(buffer: &text::BufferSnapshot) -> Self {
pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
BufferDiff {
buffer_id: buffer.remote_id(),
inner: BufferDiff::build_empty(buffer),
inner: BufferDiff::build_empty(buffer, cx),
secondary_diff: None,
}
}
@@ -939,14 +1012,10 @@ impl BufferDiff {
#[cfg(any(test, feature = "test-support"))]
pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
let base_text = self
.inner
.base_text
.as_ref()
.map(|base_text| base_text.text());
let base_text = self.base_text_string().map(Arc::new);
let snapshot = BufferDiff::build_with_base_buffer(
buffer.clone(),
base_text.clone().map(Arc::new),
base_text,
self.inner.base_text.clone(),
cx,
);
@@ -957,6 +1026,10 @@ impl BufferDiff {
}
impl DiffHunk {
pub fn is_created_file(&self) -> bool {
self.diff_base_byte_range == (0..0) && self.buffer_range == (Anchor::MIN..Anchor::MAX)
}
pub fn status(&self) -> DiffHunkStatus {
let kind = if self.buffer_range.start == self.buffer_range.end {
DiffHunkStatusKind::Deleted
@@ -973,6 +1046,23 @@ impl DiffHunk {
}
impl DiffHunkStatus {
pub fn has_secondary_hunk(&self) -> bool {
matches!(
self.secondary,
DiffHunkSecondaryStatus::HasSecondaryHunk
| DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
| DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
)
}
pub fn is_pending(&self) -> bool {
matches!(
self.secondary,
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
)
}
pub fn is_deleted(&self) -> bool {
self.kind == DiffHunkStatusKind::Deleted
}
@@ -1006,7 +1096,6 @@ impl DiffHunkStatus {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn deleted_none() -> Self {
Self {
kind: DiffHunkStatusKind::Deleted,
@@ -1014,7 +1103,6 @@ impl DiffHunkStatus {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn added_none() -> Self {
Self {
kind: DiffHunkStatusKind::Added,
@@ -1022,7 +1110,6 @@ impl DiffHunkStatus {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn modified_none() -> Self {
Self {
kind: DiffHunkStatusKind::Modified,
@@ -1045,12 +1132,10 @@ pub fn assert_hunks<Iter>(
let actual_hunks = diff_hunks
.map(|hunk| {
(
hunk.row_range.clone(),
hunk.range.clone(),
&diff_base[hunk.diff_base_byte_range.clone()],
buffer
.text_for_range(
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
)
.text_for_range(hunk.range.clone())
.collect::<String>(),
hunk.status(),
)
@@ -1059,7 +1144,14 @@ pub fn assert_hunks<Iter>(
let expected_hunks: Vec<_> = expected_hunks
.iter()
.map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
.map(|(r, old_text, new_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
*old_text,
new_text.to_string(),
*status,
)
})
.collect();
assert_eq!(actual_hunks, expected_hunks);
@@ -1120,7 +1212,7 @@ mod tests {
],
);
diff = BufferDiff::build_empty(&buffer);
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
&buffer,
@@ -1435,43 +1527,55 @@ mod tests {
for example in table {
let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
let uncommitted_diff =
BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx);
let unstaged_diff =
BufferDiff::build_sync(buffer.clone(), example.index_text.clone(), cx);
let uncommitted_diff = BufferDiffSnapshot {
inner: uncommitted_diff,
secondary_diff: Some(Box::new(BufferDiffSnapshot {
inner: unstaged_diff,
is_single_insertion: false,
secondary_diff: None,
})),
is_single_insertion: false,
};
let hunk_range =
buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
let range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
let unstaged = BufferDiff::build_sync(buffer.clone(), example.index_text.clone(), cx);
let uncommitted = BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx);
let new_index_text = cx
.update(|cx| {
uncommitted_diff.new_secondary_text_for_stage_or_unstage(
true,
uncommitted_diff
.hunks_intersecting_range(range, &buffer)
.map(|hunk| {
(hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())
}),
&buffer,
cx,
let unstaged_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
diff.set_state(unstaged, &buffer);
diff
});
let uncommitted_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
diff.set_state(uncommitted, &buffer);
diff.set_secondary_diff(unstaged_diff);
diff
});
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
}
let new_index_text = diff
.stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
.unwrap()
.to_string();
let hunks = diff
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_eq!(
hunk.secondary_status,
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
)
})
.unwrap()
.to_string();
pretty_assertions::assert_eq!(
new_index_text,
example.final_index_text,
"example: {}",
example.name
);
}
pretty_assertions::assert_eq!(
new_index_text,
example.final_index_text,
"example: {}",
example.name
);
});
}
}
@@ -1505,7 +1609,7 @@ mod tests {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
let empty_diff = BufferDiff::build_empty(&buffer);
let empty_diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
let range = diff_1.compare(&empty_diff, &buffer).unwrap();
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
@@ -1668,7 +1772,7 @@ mod tests {
index_text: &Rope,
head_text: String,
cx: &mut TestAppContext,
) -> BufferDiff {
) -> Entity<BufferDiff> {
let inner = BufferDiff::build_sync(working_copy.text.clone(), head_text, cx);
let secondary = BufferDiff {
buffer_id: working_copy.remote_id(),
@@ -1680,11 +1784,11 @@ mod tests {
secondary_diff: None,
};
let secondary = cx.new(|_| secondary);
BufferDiff {
cx.new(|_| BufferDiff {
buffer_id: working_copy.remote_id(),
inner,
secondary_diff: Some(secondary),
}
})
}
let operations = std::env::var("OPERATIONS")
@@ -1712,7 +1816,7 @@ mod tests {
};
let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
let mut hunks = cx.update(|cx| {
let mut hunks = diff.update(cx, |diff, cx| {
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
.collect::<Vec<_>>()
});
@@ -1723,6 +1827,7 @@ mod tests {
for _ in 0..operations {
let i = rng.gen_range(0..hunks.len());
let hunk = &mut hunks[i];
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::None;
@@ -1735,21 +1840,13 @@ mod tests {
_ => unreachable!(),
};
let snapshot = cx.update(|cx| diff.snapshot(cx));
index_text = cx.update(|cx| {
snapshot
.new_secondary_text_for_stage_or_unstage(
stage,
[(hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())]
.into_iter(),
&working_copy,
cx,
)
index_text = diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
.unwrap()
});
diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
let found_hunks = cx.update(|cx| {
let found_hunks = diff.update(cx, |diff, cx| {
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
.collect::<Vec<_>>()
});

View File

@@ -614,12 +614,19 @@ mod windows {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()?
} else {
std::env::current_exe()?
.parent()
.context("no parent path for cli")?
.parent()
.context("no parent path for cli folder")?
.join("Zed.exe")
let cli = std::env::current_exe()?;
let dir = cli.parent().context("no parent path for cli")?;
// ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
// directory in development builds.
let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.context(format!(
"could not find any of: {}",
possible_locations.join(", ")
))?
};
Ok(App(path))

View File

@@ -111,7 +111,7 @@ node_runtime.workspace = true
notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
prompt_library.workspace = true
prompt_store.workspace = true
recent_projects = { workspace = true }
release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }

View File

@@ -328,6 +328,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
.add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
.add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
.add_request_handler(forward_mutating_project_request::<proto::ApplyCodeActionKind>)
.add_request_handler(forward_mutating_project_request::<proto::FormatBuffers>)
.add_request_handler(forward_mutating_project_request::<proto::CreateProjectEntry>)
.add_request_handler(forward_mutating_project_request::<proto::RenameProjectEntry>)

View File

@@ -14,7 +14,7 @@ use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{channel::mpsc, StreamExt as _};
use prompt_library::PromptBuilder;
use prompt_store::PromptBuilder;
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
use gpui::{

View File

@@ -17,7 +17,7 @@ use gpui::{
ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString,
Styled, Subscription, Task, TextStyle, WeakEntity, Window,
};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
use project::{Fs, Project};
use rpc::{
proto::{self, ChannelVisibility, PeerId},
@@ -1430,7 +1430,7 @@ impl CollabPanel {
cx.notify();
}
fn select_prev(&mut self, _: &SelectPrev, _: &mut Window, cx: &mut Context<Self>) {
fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
let ix = self.selection.take().unwrap_or(0);
if ix > 0 {
self.selection = Some(ix - 1);
@@ -2878,7 +2878,7 @@ impl Render for CollabPanel {
.key_context("CollabPanel")
.on_action(cx.listener(CollabPanel::cancel))
.on_action(cx.listener(CollabPanel::select_next))
.on_action(cx.listener(CollabPanel::select_prev))
.on_action(cx.listener(CollabPanel::select_previous))
.on_action(cx.listener(CollabPanel::confirm))
.on_action(cx.listener(CollabPanel::insert_space))
.on_action(cx.listener(CollabPanel::remove_selected_channel))

View File

@@ -22,7 +22,7 @@ use ui::{
h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,
};
use util::{ResultExt, TryFutureExt};
use workspace::notifications::NotificationId;
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
@@ -570,11 +570,12 @@ impl NotificationPanel {
workspace.dismiss_notification(&id, cx);
workspace.show_notification(id, cx, |cx| {
let workspace = cx.entity().downgrade();
cx.new(|_| NotificationToast {
cx.new(|cx| NotificationToast {
notification_id,
actor,
text,
workspace,
focus_handle: cx.focus_handle(),
})
})
})
@@ -771,8 +772,17 @@ pub struct NotificationToast {
actor: Option<Arc<User>>,
text: String,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
}
impl Focusable for NotificationToast {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl WorkspaceNotification for NotificationToast {}
impl NotificationToast {
fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
let workspace = self.workspace.clone();

View File

@@ -18,7 +18,7 @@ pub trait Component {
}
pub trait ComponentPreview: Component {
fn preview(_window: &mut Window, _cx: &App) -> AnyElement;
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
}
#[distributed_slice]
@@ -32,7 +32,7 @@ pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
pub struct ComponentRegistry {
components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
}
impl ComponentRegistry {
@@ -62,7 +62,10 @@ pub fn register_component<T: Component>() {
}
pub fn register_preview<T: ComponentPreview>() {
let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement);
let preview_data = (
T::name(),
T::preview as fn(&mut Window, &mut App) -> AnyElement,
);
COMPONENT_DATA
.write()
.previews
@@ -77,7 +80,7 @@ pub struct ComponentMetadata {
name: SharedString,
scope: Option<SharedString>,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &App) -> AnyElement>,
preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
}
impl ComponentMetadata {
@@ -93,7 +96,7 @@ impl ComponentMetadata {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> AnyElement> {
self.preview
}
}
@@ -235,6 +238,7 @@ pub struct ComponentExampleGroup {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample>,
pub grow: bool,
pub vertical: bool,
}
impl RenderOnce for ComponentExampleGroup {
@@ -270,6 +274,7 @@ impl RenderOnce for ComponentExampleGroup {
.child(
div()
.flex()
.when(self.vertical, |this| this.flex_col())
.items_start()
.w_full()
.gap_6()
@@ -287,6 +292,7 @@ impl ComponentExampleGroup {
title: None,
examples,
grow: false,
vertical: false,
}
}
@@ -296,6 +302,7 @@ impl ComponentExampleGroup {
title: Some(title.into()),
examples,
grow: false,
vertical: false,
}
}
@@ -304,6 +311,12 @@ impl ComponentExampleGroup {
self.grow = true;
self
}
/// Lay the group out vertically.
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
/// Create a single example

View File

@@ -93,7 +93,7 @@ impl ComponentPreview {
&self,
ix: usize,
window: &mut Window,
cx: &Context<Self>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let component = self.get_component(ix);
@@ -226,3 +226,7 @@ impl Item for ComponentPreview {
f(*event)
}
}
// TODO: impl serializable item for component preview so it will restore with the workspace
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler

View File

@@ -51,6 +51,7 @@ impl Tool for ContextServerTool {
fn run(
self: std::sync::Arc<Self>,
input: serde_json::Value,
_thread_id: Arc<str>,
_workspace: gpui::WeakEntity<workspace::Workspace>,
_: &mut Window,
cx: &mut App,

View File

@@ -995,7 +995,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
h_flex()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
StyledText::new(message.clone()).with_default_highlights(
&cx.window.text_style(),
code_ranges
.iter()

View File

@@ -35,6 +35,13 @@ pub struct SelectToBeginningOfLine {
pub stop_at_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DeleteToBeginningOfLine {
#[serde(default)]
pub(super) stop_at_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MovePageUp {
@@ -212,6 +219,7 @@ impl_actions!(
ComposeCompletion,
ConfirmCodeAction,
ConfirmCompletion,
DeleteToBeginningOfLine,
DeleteToNextWordEnd,
DeleteToPreviousWordStart,
ExpandExcerpts,
@@ -257,7 +265,7 @@ gpui::actions!(
ContextMenuFirst,
ContextMenuLast,
ContextMenuNext,
ContextMenuPrev,
ContextMenuPrevious,
ConvertToKebabCase,
ConvertToLowerCamelCase,
ConvertToLowerCase,
@@ -276,7 +284,6 @@ gpui::actions!(
CutToEndOfLine,
Delete,
DeleteLine,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
DeleteToNextSubwordEnd,
DeleteToPreviousSubwordStart,
@@ -301,10 +308,10 @@ gpui::actions!(
GoToDefinitionSplit,
GoToDiagnostic,
GoToHunk,
GoToPreviousHunk,
GoToImplementation,
GoToImplementationSplit,
GoToPrevDiagnostic,
GoToPrevHunk,
GoToPreviousDiagnostic,
GoToTypeDefinition,
GoToTypeDefinitionSplit,
HalfPageDown,
@@ -348,6 +355,7 @@ gpui::actions!(
OpenPermalinkToLine,
OpenSelectionsInMultibuffer,
OpenUrl,
OrganizeImports,
Outdent,
AutoIndent,
PageDown,
@@ -398,7 +406,7 @@ gpui::actions!(
SplitSelectionIntoLines,
SwitchSourceHeader,
Tab,
TabPrev,
Backtab,
ToggleAutoSignatureHelp,
ToggleGitBlame,
ToggleGitBlameInline,

View File

@@ -514,7 +514,7 @@ impl CompletionsMenu {
);
let completion_label = StyledText::new(completion.label.text.clone())
.with_highlights(&style.text, highlights);
.with_default_highlights(&style.text, highlights);
let documentation_label = if let Some(
CompletionDocumentation::SingleLine(text),
) = documentation

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};
use invisibles::{is_invisible, replacement};
pub use invisibles::{is_invisible, replacement};
use language::{
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,
@@ -1124,6 +1124,11 @@ impl DisplaySnapshot {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
self.block_snapshot
.is_folded_buffer_header(BlockRow(display_row.0))
}
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
let wrap_row = self
.block_snapshot

View File

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

View File

@@ -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(crate) fn replacement(c: char) -> Option<&'static str> {
pub fn replacement(c: char) -> Option<&'static str> {
if c <= '\x1f' {
Some(C0_SYMBOLS[c as usize])
} else if c == '\x7f' {

View File

@@ -52,7 +52,7 @@ pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit};
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus};
use buffer_diff::DiffHunkStatus;
use client::{Collaborator, ParticipantIndex};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
@@ -73,7 +73,7 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use ::git::{status::FileStatus, Restore};
use ::git::Restore;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
@@ -85,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, Styled, StyledText, Subscription, Task,
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
WeakEntity, WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
@@ -120,8 +120,8 @@ use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
pub use lsp::CompletionContext;
use lsp::{
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
LanguageServerId, LanguageServerName,
CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
InsertTextFormat, LanguageServerId, LanguageServerName,
};
use language::BufferSnapshot;
@@ -203,6 +203,7 @@ pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -2232,6 +2233,43 @@ impl Editor {
cx.notify();
}
pub fn sync_selections(
&mut self,
other: Entity<Editor>,
cx: &mut Context<Self>,
) -> gpui::Subscription {
let other_selections = other.read(cx).selections.disjoint.to_vec();
self.selections.change_with(cx, |selections| {
selections.select_anchors(other_selections);
});
let other_subscription =
cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
EditorEvent::SelectionsChanged { local: true } => {
let other_selections = other.read(cx).selections.disjoint.to_vec();
this.selections.change_with(cx, |selections| {
selections.select_anchors(other_selections);
});
}
_ => {}
});
let this_subscription =
cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
EditorEvent::SelectionsChanged { local: true } => {
let these_selections = this.selections.disjoint.to_vec();
other.update(cx, |other_editor, cx| {
other_editor.selections.change_with(cx, |selections| {
selections.select_anchors(these_selections);
})
});
}
_ => {}
});
Subscription::join(other_subscription, this_subscription)
}
pub fn change_selections<R>(
&mut self,
autoscroll: Option<Autoscroll>,
@@ -2876,7 +2914,7 @@ impl Editor {
}
if let Some(bracket_pair) = bracket_pair {
let snapshot_settings = snapshot.settings_at(selection.start, cx);
let snapshot_settings = snapshot.language_settings_at(selection.start, cx);
let autoclose = self.use_autoclose && snapshot_settings.use_autoclose;
let auto_surround =
self.use_auto_surround && snapshot_settings.use_auto_surround;
@@ -2941,7 +2979,7 @@ impl Editor {
}
let always_treat_brackets_as_autoclosed = snapshot
.settings_at(selection.start, cx)
.language_settings_at(selection.start, cx)
.always_treat_brackets_as_autoclosed;
if always_treat_brackets_as_autoclosed
&& is_bracket_pair_end
@@ -3224,7 +3262,7 @@ impl Editor {
return None;
}
if !multi_buffer.settings_at(0, cx).extend_comment_on_newline {
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
return None;
}
@@ -3553,7 +3591,7 @@ impl Editor {
}
let always_treat_brackets_as_autoclosed = buffer
.settings_at(selection.start, cx)
.language_settings_at(selection.start, cx)
.always_treat_brackets_as_autoclosed;
if !always_treat_brackets_as_autoclosed {
@@ -3690,6 +3728,7 @@ impl Editor {
InlayHintRefreshReason::SettingsChange(_)
| InlayHintRefreshReason::Toggle(_)
| InlayHintRefreshReason::ExcerptsRemoved(_)
| InlayHintRefreshReason::ModifiersChanged(_)
);
let (invalidate_cache, required_languages) = match reason {
InlayHintRefreshReason::ModifiersChanged(enabled) => {
@@ -6292,6 +6331,9 @@ 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(
@@ -6322,7 +6364,19 @@ impl Editor {
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(self.render_edit_prediction_accept_keybind(window, cx)),
.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),
)
.into_any();
@@ -6420,7 +6474,11 @@ impl Editor {
}
}
fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
fn render_edit_prediction_accept_keybind(
&self,
window: &mut Window,
cx: &App,
) -> Option<AnyElement> {
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
let accept_keystroke = accept_binding.keystroke()?;
@@ -6456,6 +6514,7 @@ impl Editor {
.size(Some(IconSize::XSmall.rems().into())),
)
})
.into_any()
.into()
}
@@ -6465,10 +6524,14 @@ impl Editor {
icon: Option<IconName>,
window: &mut Window,
cx: &App,
) -> Option<Div> {
) -> Option<Stateful<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)
@@ -6478,8 +6541,35 @@ impl Editor {
.bg(Self::edit_prediction_line_popover_bg_color(cx))
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
.shadow_sm()
.children(self.render_edit_prediction_accept_keybind(window, cx))
.child(Label::new(label).size(LabelSize::Small))
.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()),
),
)
})
.when_some(icon, |element, icon| {
element.child(
div()
@@ -6576,6 +6666,9 @@ 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()
@@ -6604,16 +6697,37 @@ impl Editor {
el.child(
Label::new("Hold")
.size(LabelSize::Small)
.when(accept_keystroke.is_none(), |el| {
el.strikethrough()
})
.line_height_style(LineHeightStyle::UiLabel),
)
})
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke?.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
false,
))),
.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,
)))
},
),
)
.into_any(),
);
@@ -6779,7 +6893,7 @@ impl Editor {
.first_line_preview();
let styled_text = gpui::StyledText::new(highlighted_edits.text)
.with_highlights(&style.text, highlighted_edits.highlights);
.with_default_highlights(&style.text, highlighted_edits.highlights);
let preview = h_flex()
.gap_1()
@@ -7190,7 +7304,7 @@ impl Editor {
});
}
pub fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
if self.move_to_prev_snippet_tabstop(window, cx) {
return;
}
@@ -7251,7 +7365,7 @@ impl Editor {
}
// Otherwise, insert a hard or soft tab.
let settings = buffer.settings_at(cursor, cx);
let settings = buffer.language_settings_at(cursor, cx);
let tab_size = if settings.hard_tabs {
IndentSize::tab()
} else {
@@ -7315,7 +7429,7 @@ impl Editor {
delta_for_start_row: u32,
cx: &App,
) -> u32 {
let settings = buffer.settings_at(selection.start, cx);
let settings = buffer.language_settings_at(selection.start, cx);
let tab_size = settings.tab_size.get();
let indent_kind = if settings.hard_tabs {
IndentKind::Tab
@@ -7395,7 +7509,7 @@ impl Editor {
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
for selection in &selections {
let settings = buffer.settings_at(selection.start, cx);
let settings = buffer.language_settings_at(selection.start, cx);
let tab_size = settings.tab_size.get();
let mut rows = selection.spanned_rows(false, &display_map);
@@ -7719,14 +7833,9 @@ impl Editor {
cx: &mut Context<Editor>,
) {
let mut revert_changes = HashMap::default();
let snapshot = self.buffer.read(cx).snapshot(cx);
let Some(project) = &self.project else {
return;
};
let chunk_by = self
.snapshot(window, cx)
.hunks_for_ranges(ranges.into_iter())
.hunks_for_ranges(ranges)
.into_iter()
.chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
@@ -7734,15 +7843,7 @@ impl Editor {
for hunk in &hunks {
self.prepare_restore_change(&mut revert_changes, hunk, cx);
}
Self::do_stage_or_unstage(
project,
false,
buffer_id,
hunks.into_iter(),
&snapshot,
window,
cx,
);
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), window, cx);
}
drop(chunk_by);
if !revert_changes.is_empty() {
@@ -7787,7 +7888,6 @@ impl Editor {
let original_text = diff
.read(cx)
.base_text()
.as_ref()?
.as_rope()
.slice(hunk.diff_base_byte_range.clone());
let buffer_snapshot = buffer.snapshot();
@@ -8421,7 +8521,7 @@ impl Editor {
continue;
}
let tab_size = buffer.settings_at(selection.head(), cx).tab_size;
let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size;
// Since not all lines in the selection may be at the same indent
// level, choose the indent size that is the most common between all
@@ -8467,7 +8567,7 @@ impl Editor {
inside_comment = true;
}
let language_settings = buffer.settings_at(selection.head(), cx);
let language_settings = buffer.language_settings_at(selection.head(), cx);
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
RewrapBehavior::InComments => inside_comment,
RewrapBehavior::InSelections => !selection.is_empty(),
@@ -8523,7 +8623,7 @@ impl Editor {
};
let wrap_column = buffer
.settings_at(Point::new(start_row, 0), cx)
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize;
let wrapped_text = wrap_with_prefix(
line_prefix,
@@ -8709,8 +8809,9 @@ impl Editor {
this.buffer.update(cx, |buffer, cx| {
let snapshot = buffer.read(cx);
auto_indent_on_paste =
snapshot.settings_at(cursor_offset, cx).auto_indent_on_paste;
auto_indent_on_paste = snapshot
.language_settings_at(cursor_offset, cx)
.auto_indent_on_paste;
let mut start_offset = 0;
let mut edits = Vec::new();
@@ -9248,7 +9349,7 @@ impl Editor {
pub fn context_menu_prev(
&mut self,
_: &ContextMenuPrev,
_: &ContextMenuPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -9528,7 +9629,7 @@ impl Editor {
pub fn delete_to_beginning_of_line(
&mut self,
_: &DeleteToBeginningOfLine,
action: &DeleteToBeginningOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -9542,7 +9643,7 @@ impl Editor {
this.select_to_beginning_of_line(
&SelectToBeginningOfLine {
stop_at_soft_wraps: false,
stop_at_indent: false,
stop_at_indent: action.stop_at_indent,
},
window,
cx,
@@ -11275,7 +11376,7 @@ impl Editor {
fn go_to_prev_diagnostic(
&mut self,
_: &GoToPrevDiagnostic,
_: &GoToPreviousDiagnostic,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -11427,63 +11528,84 @@ 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_position(&snapshot, selection.head(), window, cx);
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
Direction::Next,
window,
cx,
);
}
fn go_to_hunk_after_position(
fn go_to_hunk_after_or_before_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
direction: Direction,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let row = if direction == Direction::Next {
self.hunk_after_position(snapshot, position)
.map(|hunk| hunk.row_range.start)
} else {
self.hunk_before_position(snapshot, position)
};
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]);
});
}
}
fn hunk_after_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
) -> Option<MultiBufferDiffHunk> {
let mut hunk = snapshot
snapshot
.buffer_snapshot
.diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point())
.find(|hunk| hunk.row_range.start.0 > position.row);
if hunk.is_none() {
hunk = snapshot
.buffer_snapshot
.diff_hunks_in_range(Point::zero()..position)
.find(|hunk| hunk.row_range.end.0 < position.row)
}
if let Some(hunk) = &hunk {
let destination = Point::new(hunk.row_range.start.0, 0);
self.unfold_ranges(&[destination..destination], false, false, cx);
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_ranges(vec![destination..destination]);
});
}
hunk
.find(|hunk| hunk.row_range.start.0 > position.row)
.or_else(|| {
snapshot
.buffer_snapshot
.diff_hunks_in_range(Point::zero()..position)
.find(|hunk| hunk.row_range.end.0 < position.row)
})
}
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, window: &mut Window, cx: &mut Context<Self>) {
fn go_to_prev_hunk(
&mut self,
_: &GoToPreviousHunk,
window: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_before_position(&snapshot, selection.head(), window, cx);
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
Direction::Prev,
window,
cx,
);
}
fn go_to_hunk_before_position(
fn hunk_before_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<MultiBufferDiffHunk> {
let mut hunk = snapshot.buffer_snapshot.diff_hunk_before(position);
if hunk.is_none() {
hunk = snapshot.buffer_snapshot.diff_hunk_before(Point::MAX);
}
if let Some(hunk) = &hunk {
let destination = Point::new(hunk.row_range.start.0, 0);
self.unfold_ranges(&[destination..destination], false, false, cx);
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_ranges(vec![destination..destination]);
});
}
hunk
) -> Option<MultiBufferRow> {
snapshot
.buffer_snapshot
.diff_hunk_before(position)
.or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
}
pub fn go_to_definition(
@@ -12494,7 +12616,6 @@ impl Editor {
buffer.push_transaction(&transaction.0, cx);
}
}
cx.notify();
})
.ok();
@@ -12503,6 +12624,60 @@ impl Editor {
})
}
fn organize_imports(
&mut self,
_: &OrganizeImports,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
let project = match &self.project {
Some(project) => project.clone(),
None => return None,
};
Some(self.perform_code_action_kind(
project,
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
window,
cx,
))
}
fn perform_code_action_kind(
&mut self,
project: Entity<Project>,
kind: CodeActionKind,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let buffer = self.buffer.clone();
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse();
let apply_action = project.update(cx, |project, cx| {
project.apply_code_action_kind(buffers, kind, true, cx)
});
cx.spawn_in(window, |_, mut cx| async move {
let transaction = futures::select_biased! {
() = timeout => {
log::warn!("timed out waiting for executing code action");
None
}
transaction = apply_action.log_err().fuse() => transaction,
};
buffer
.update(&mut cx, |buffer, cx| {
// check if we need this
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0, cx);
}
}
cx.notify();
})
.ok();
Ok(())
})
}
fn restart_language_server(
&mut self,
_: &RestartLanguageServer,
@@ -12895,13 +13070,18 @@ impl Editor {
}
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_ids: HashSet<_> = multi_buffer_snapshot
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
.map(|(snapshot, _, _)| snapshot.remote_id())
let buffer_ids: HashSet<_> = self
.selections
.disjoint_anchor_ranges()
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
.collect();
let should_unfold = buffer_ids
.iter()
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
for buffer_id in buffer_ids {
if self.is_buffer_folded(buffer_id, cx) {
if should_unfold {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
@@ -12978,11 +13158,11 @@ impl Editor {
self.fold_creases(to_fold, true, window, cx);
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_ids: HashSet<_> = multi_buffer_snapshot
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
.map(|(snapshot, _, _)| snapshot.remote_id())
.collect();
let buffer_ids = self
.selections
.disjoint_anchor_ranges()
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
.collect::<HashSet<_>>();
for buffer_id in buffer_ids {
self.fold_buffer(buffer_id, cx);
}
@@ -13155,10 +13335,11 @@ impl Editor {
self.unfold_ranges(&ranges, true, true, cx);
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_ids: HashSet<_> = multi_buffer_snapshot
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
.map(|(snapshot, _, _)| snapshot.remote_id())
.collect();
let buffer_ids = self
.selections
.disjoint_anchor_ranges()
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
.collect::<HashSet<_>>();
for buffer_id in buffer_ids {
self.unfold_buffer(buffer_id, cx);
}
@@ -13470,7 +13651,7 @@ impl Editor {
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot);
hunks.any(|hunk| hunk.secondary_status != DiffHunkSecondaryStatus::None)
hunks.any(|hunk| hunk.status().has_secondary_hunk())
}
pub fn toggle_staged_selected_diff_hunks(
@@ -13511,15 +13692,11 @@ impl Editor {
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let Some(project) = &self.project else {
return;
};
let chunk_by = self
.diff_hunks_in_ranges(&ranges, &snapshot)
.chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
Self::do_stage_or_unstage(project, stage, buffer_id, hunks, &snapshot, window, cx);
self.do_stage_or_unstage(stage, buffer_id, hunks, window, cx);
}
}
@@ -13529,80 +13706,47 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
if ranges.iter().any(|range| range.start != range.end) {
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
return;
}
if !self.buffer().read(cx).is_singleton() {
if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) {
if buffer.read(cx).is_empty() {
let buffer = buffer.read(cx);
let Some(file) = buffer.file() else {
return;
};
let project_path = project::ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
};
let Some(project) = self.project.as_ref() else {
return;
};
let project = project.read(cx);
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let Some(repo) = project.git_store().read(cx).active_repository() else {
return;
};
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)
});
repo.update(cx, |repo, cx| {
let Some(repo_path) = repo.project_path_to_repo_path(&project_path) else {
return;
};
let Some(status) = repo.repository_entry.status_for_path(&repo_path) else {
return;
};
if stage && status.status == FileStatus::Untracked {
repo.stage_entries(vec![repo_path], cx)
.detach_and_log_err(cx);
return;
}
})
}
ranges = vec![multi_buffer::Anchor::range_in_buffer(
excerpt_id,
buffer.read(cx).remote_id(),
range,
)];
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
let snapshot = self.buffer().read(cx).snapshot(cx);
let mut point = ranges.last().unwrap().end.to_point(&snapshot);
if point.row < snapshot.max_row().0 {
point.row += 1;
point.column = 0;
point = snapshot.clip_point(point, Bias::Right);
self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| {
s.select_ranges([point..point]);
})
}
return;
}
if run_twice {
self.go_to_next_hunk(&GoToHunk, window, cx);
}
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
self.go_to_next_hunk(&Default::default(), window, cx);
self.go_to_next_hunk(&GoToHunk, window, cx);
}
fn do_stage_or_unstage(
project: &Entity<Project>,
&self,
stage: bool,
buffer_id: BufferId,
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
snapshot: &MultiBufferSnapshot,
window: &mut Window,
cx: &mut App,
) {
let Some(project) = self.project.as_ref() else {
return;
};
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
log::debug!("no buffer for id");
return;
};
let Some(diff) = self.buffer.read(cx).diff_for(buffer_id) else {
return;
};
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -13616,37 +13760,31 @@ impl Editor {
log::debug!("no git repo for buffer id");
return;
};
let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
log::debug!("no diff for buffer id");
return;
};
let new_index_text = if !stage && diff.is_single_insertion || stage && !file_exists {
log::debug!("removing from index");
None
} else {
diff.new_secondary_text_for_stage_or_unstage(
let new_index_text = diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(
stage,
hunks.filter_map(|hunk| {
if stage && hunk.secondary_status == DiffHunkSecondaryStatus::None {
return None;
} else if !stage
&& hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk
{
return None;
}
Some((hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone()))
}),
&hunks
.map(|hunk| buffer_diff::DiffHunk {
buffer_range: hunk.buffer_range,
diff_base_byte_range: hunk.diff_base_byte_range,
secondary_status: hunk.secondary_status,
range: Point::zero()..Point::zero(), // unused
})
.collect::<Vec<_>>(),
&buffer_snapshot,
file_exists,
cx,
)
};
});
if file_exists {
let buffer_store = project.read(cx).buffer_store().clone();
buffer_store
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
.detach_and_log_err(cx);
}
let recv = repo
.read(cx)
.set_index_text(&path, new_index_text.map(|rope| rope.to_string()));
@@ -13721,7 +13859,7 @@ impl Editor {
cx: &mut Context<Self>,
) {
let snapshot = self.snapshot(window, cx);
let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx).into_iter());
let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx));
let mut ranges_by_buffer = HashMap::default();
self.transact(window, cx, |editor, _window, cx| {
for hunk in hunks {
@@ -13905,12 +14043,16 @@ impl Editor {
return wrap_guides;
}
let settings = self.buffer.read(cx).settings_at(0, cx);
let settings = self.buffer.read(cx).language_settings(cx);
if settings.show_wrap_guides {
if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
wrap_guides.push((soft_wrap as usize, true));
} else if let SoftWrap::Bounded(soft_wrap) = self.soft_wrap_mode(cx) {
wrap_guides.push((soft_wrap as usize, true));
match self.soft_wrap_mode(cx) {
SoftWrap::Column(soft_wrap) => {
wrap_guides.push((soft_wrap as usize, true));
}
SoftWrap::Bounded(soft_wrap) => {
wrap_guides.push((soft_wrap as usize, true));
}
SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {}
}
wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
}
@@ -13919,7 +14061,7 @@ impl Editor {
}
pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
let settings = self.buffer.read(cx).settings_at(0, cx);
let settings = self.buffer.read(cx).language_settings(cx);
let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap);
match mode {
language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => {
@@ -14018,7 +14160,7 @@ impl Editor {
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
self.buffer
.read(cx)
.settings_at(0, cx)
.language_settings(cx)
.indent_guides
.enabled
});
@@ -15667,7 +15809,7 @@ impl Editor {
let copilot_enabled_for_language = self
.buffer
.read(cx)
.settings_at(0, cx)
.language_settings(cx)
.show_edit_predictions;
let project = project.read(cx);
@@ -15939,9 +16081,9 @@ impl Editor {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes.into_iter().map(|(range, text)| {
(range, text.to_string().map(Arc::<str>::from))
}),
changes
.into_iter()
.map(|(range, text)| (range, text.to_string())),
None,
cx,
);
@@ -17048,7 +17190,7 @@ impl EditorSnapshot {
pub fn hunks_for_ranges(
&self,
ranges: impl Iterator<Item = Range<Point>>,
ranges: impl IntoIterator<Item = Range<Point>>,
) -> Vec<MultiBufferDiffHunk> {
let mut hunks = Vec::new();
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
@@ -17059,17 +17201,14 @@ impl EditorSnapshot {
for hunk in self.buffer_snapshot.diff_hunks_in_range(
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk.status().is_deleted();
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
hunk.row_range.overlaps(&query_rows)
};
if related_to_selection {
// Include deleted hunks that are adjacent to the query range, because
// otherwise they would be missed.
let mut intersects_range = hunk.row_range.overlaps(&query_rows);
if hunk.status().is_deleted() {
intersects_range |= hunk.row_range.start == query_rows.end;
intersects_range |= hunk.row_range.end == query_rows.start;
}
if intersects_range {
if !processed_buffer_rows
.entry(hunk.buffer_id)
.or_default()
@@ -17910,7 +18049,7 @@ pub fn diagnostic_block_renderer(
)
.child(buttons(&diagnostic))
.child(div().flex().flex_shrink_0().child(
StyledText::new(text_without_backticks.clone()).with_highlights(
StyledText::new(text_without_backticks.clone()).with_default_highlights(
&text_style,
code_ranges.iter().map(|range| {
(
@@ -18229,3 +18368,37 @@ fn all_edits_insertions_or_deletions(
}
all_insertions || all_deletions
}
struct MissingEditPredictionKeybindingTooltip;
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");
})),
)
})
}
}

View File

@@ -7,7 +7,7 @@ use crate::{
},
JoinLines,
};
use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use futures::StreamExt;
use gpui::{
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
@@ -1514,6 +1514,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
stop_at_indent: true,
};
let delete_to_beg = DeleteToBeginningOfLine {
stop_at_indent: false,
};
let move_to_end = MoveToEndOfLine {
stop_at_soft_wraps: true,
};
@@ -1672,7 +1676,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
assert_eq!(editor.display_text(cx), "\n");
assert_eq!(
editor.selections.display_ranges(cx),
@@ -1778,6 +1782,107 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let move_to_beg = MoveToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
};
let select_to_beg = SelectToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
};
let delete_to_beg = DeleteToBeginningOfLine {
stop_at_indent: true,
};
let move_to_end = MoveToEndOfLine {
stop_at_soft_wraps: false,
};
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("abc\n def", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
]);
});
// Moving to the beginning of the line should put the first cursor at the beginning of the line,
// and the second cursor at the first non-whitespace character in the line.
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
]
);
// Moving to the beginning of the line again should be a no-op for the first cursor,
// and should move the second cursor to the beginning of the line.
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
]
);
// Moving to the beginning of the line again should still be a no-op for the first cursor,
// and should move the second cursor back to the first non-whitespace character in the line.
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
]
);
// Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
// and to the first non-whitespace character in the line for the second cursor.
editor.move_to_end_of_line(&move_to_end, window, cx);
editor.move_left(&MoveLeft, window, cx);
editor.select_to_beginning_of_line(&select_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
]
);
// Selecting to the beginning of the line again should be a no-op for the first cursor,
// and should select to the beginning of the line for the second cursor.
editor.select_to_beginning_of_line(&select_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
]
);
// Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
// and should delete to the first non-whitespace character in the line for the second cursor.
editor.move_to_end_of_line(&move_to_end, window, cx);
editor.move_left(&MoveLeft, window, cx);
editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
assert_eq!(editor.text(cx), "c\n f");
});
}
#[gpui::test]
fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -2295,7 +2400,13 @@ async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("one «two threeˇ» four");
cx.update_editor(|editor, window, cx| {
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
editor.delete_to_beginning_of_line(
&DeleteToBeginningOfLine {
stop_at_indent: false,
},
window,
cx,
);
assert_eq!(editor.text(cx), " four");
});
}
@@ -2854,7 +2965,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
«oneˇ» «twoˇ»
three
@@ -2874,7 +2985,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
ˇ» four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
t«hree
@@ -2899,7 +3010,7 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
ˇ three
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
ˇthree
@@ -2933,13 +3044,13 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
three
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
\t«oneˇ» «twoˇ»
three
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
«oneˇ» «twoˇ»
three
@@ -2964,13 +3075,13 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
\t\tt«hree
ˇ»four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
\tt«hree
ˇ»four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
t«hree
@@ -2983,7 +3094,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
ˇthree
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
ˇthree
@@ -2995,7 +3106,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
\tˇthree
four
"});
cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
cx.assert_editor_state(indoc! {"
one two
ˇthree
@@ -3100,7 +3211,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
"},
cx,
);
editor.tab_prev(&TabPrev, window, cx);
editor.backtab(&Backtab, window, cx);
assert_text_with_selections(
&mut editor,
indoc! {"
@@ -7875,6 +7986,157 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
))
});
let fs = FakeFs::new(cx.executor());
fs.insert_file(path!("/file.ts"), Default::default()).await;
let project = Project::test(fs, [path!("/").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
..LanguageConfig::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)));
update_test_language_settings(cx, |settings| {
settings.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
let mut fake_servers = language_registry.register_fake_lsp(
"TypeScript",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
..Default::default()
},
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/file.ts"), cx)
})
.await
.unwrap();
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| {
build_editor_with_project(project.clone(), buffer, window, cx)
});
editor.update_in(cx, |editor, window, cx| {
editor.set_text(
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
window,
cx,
)
});
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let format = editor
.update_in(cx, |editor, window, cx| {
editor.perform_code_action_kind(
project.clone(),
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
window,
cx,
)
})
.unwrap();
fake_server
.handle_request::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/file.ts")).unwrap()
);
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
lsp::CodeAction {
title: "Organize Imports".to_string(),
kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
edit: Some(lsp::WorkspaceEdit {
changes: Some(
[(
params.text_document.uri.clone(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 0),
lsp::Position::new(2, 0),
),
"".to_string(),
)],
)]
.into_iter()
.collect(),
),
..Default::default()
}),
..Default::default()
},
)]))
})
.next()
.await;
cx.executor().start_waiting();
format.await;
assert_eq!(
editor.update(cx, |editor, cx| editor.text(cx)),
"import { a } from 'module';\n\nconst x = a;\n"
);
editor.update_in(cx, |editor, window, cx| {
editor.set_text(
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
window,
cx,
)
});
// Ensure we don't lock if code action hangs.
fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/file.ts")).unwrap()
);
futures::future::pending::<()>().await;
unreachable!()
},
);
let format = editor
.update_in(cx, |editor, window, cx| {
editor.perform_code_action_kind(
project,
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
window,
cx,
)
})
.unwrap();
cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
cx.executor().start_waiting();
format.await;
assert_eq!(
editor.update(cx, |editor, cx| editor.text(cx)),
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
);
}
#[gpui::test]
async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -10764,7 +11026,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -10773,7 +11035,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -10782,7 +11044,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -10791,7 +11053,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -10871,7 +11133,7 @@ async fn cycle_through_same_place_diagnostics(
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -10880,7 +11142,7 @@ async fn cycle_through_same_place_diagnostics(
// Third diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -10889,7 +11151,7 @@ async fn cycle_through_same_place_diagnostics(
// Second diagnostic, same place
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -10898,7 +11160,7 @@ async fn cycle_through_same_place_diagnostics(
// First diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -10907,7 +11169,7 @@ async fn cycle_through_same_place_diagnostics(
// Wrapped over, fourth diagnostic
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -11173,7 +11435,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
cx.update_editor(|editor, window, cx| {
//Wrap around the top of the buffer
for _ in 0..2 {
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
}
});
@@ -11193,7 +11455,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
);
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
});
cx.assert_editor_state(
@@ -11212,7 +11474,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
);
cx.update_editor(|editor, window, cx| {
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
});
cx.assert_editor_state(
@@ -11232,7 +11494,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
cx.update_editor(|editor, window, cx| {
for _ in 0..2 {
editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
}
});
@@ -11967,7 +12229,7 @@ async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.context_menu_prev(&ContextMenuPrev, window, cx);
editor.context_menu_prev(&ContextMenuPrevious, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -12157,7 +12419,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
resolved_items.lock().clear();
cx.update_editor(|editor, window, cx| {
editor.context_menu_prev(&ContextMenuPrev, window, cx);
editor.context_menu_prev(&ContextMenuPrevious, window, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
@@ -12404,7 +12666,7 @@ async fn test_addition_reverts(cx: &mut TestAppContext) {
struct Row9.2;
struct Row9.3;
struct Row10;"#},
vec![DiffHunkStatus::added_none(), DiffHunkStatus::added_none()],
vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
indoc! {r#"struct Row;
struct Row1;
struct Row1.1;
@@ -12442,7 +12704,7 @@ async fn test_addition_reverts(cx: &mut TestAppContext) {
struct Row8;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::added_none(), DiffHunkStatus::added_none()],
vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
indoc! {r#"struct Row;
struct Row1;
struct Row2;
@@ -12489,11 +12751,11 @@ async fn test_addition_reverts(cx: &mut TestAppContext) {
«ˇ// something on bottom»
struct Row10;"#},
vec![
DiffHunkStatus::added_none(),
DiffHunkStatus::added_none(),
DiffHunkStatus::added_none(),
DiffHunkStatus::added_none(),
DiffHunkStatus::added_none(),
DiffHunkStatusKind::Added,
DiffHunkStatusKind::Added,
DiffHunkStatusKind::Added,
DiffHunkStatusKind::Added,
DiffHunkStatusKind::Added,
],
indoc! {r#"struct Row;
ˇstruct Row1;
@@ -12541,10 +12803,7 @@ async fn test_modification_reverts(cx: &mut TestAppContext) {
struct Row99;
struct Row9;
struct Row10;"#},
vec![
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
],
vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
@@ -12571,10 +12830,7 @@ async fn test_modification_reverts(cx: &mut TestAppContext) {
struct Row99;
struct Row9;
struct Row10;"#},
vec![
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
],
vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
@@ -12603,12 +12859,12 @@ async fn test_modification_reverts(cx: &mut TestAppContext) {
struct Row9;
struct Row1011;ˇ"#},
vec![
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
DiffHunkStatus::modified_none(),
DiffHunkStatusKind::Modified,
DiffHunkStatusKind::Modified,
DiffHunkStatusKind::Modified,
DiffHunkStatusKind::Modified,
DiffHunkStatusKind::Modified,
DiffHunkStatusKind::Modified,
],
indoc! {r#"struct Row;
ˇstruct Row1;
@@ -12686,10 +12942,7 @@ struct Row10;"#};
ˇ
struct Row8;
struct Row10;"#},
vec![
DiffHunkStatus::deleted_none(),
DiffHunkStatus::deleted_none(),
],
vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
indoc! {r#"struct Row;
struct Row2;
@@ -12712,10 +12965,7 @@ struct Row10;"#};
ˇ»
struct Row8;
struct Row10;"#},
vec![
DiffHunkStatus::deleted_none(),
DiffHunkStatus::deleted_none(),
],
vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
indoc! {r#"struct Row;
struct Row2;
@@ -12740,10 +12990,7 @@ struct Row10;"#};
struct Row8;ˇ
struct Row10;"#},
vec![
DiffHunkStatus::deleted_none(),
DiffHunkStatus::deleted_none(),
],
vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
indoc! {r#"struct Row;
struct Row1;
ˇstruct Row2;
@@ -12768,9 +13015,9 @@ struct Row10;"#};
struct Row8;ˇ»
struct Row10;"#},
vec![
DiffHunkStatus::deleted_none(),
DiffHunkStatus::deleted_none(),
DiffHunkStatus::deleted_none(),
DiffHunkStatusKind::Deleted,
DiffHunkStatusKind::Deleted,
DiffHunkStatusKind::Deleted,
],
indoc! {r#"struct Row;
struct Row1;
@@ -16687,14 +16934,13 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
#[track_caller]
fn assert_hunk_revert(
not_reverted_text_with_selections: &str,
expected_hunk_statuses_before: Vec<DiffHunkStatus>,
expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
expected_reverted_text_with_selections: &str,
base_text: &str,
cx: &mut EditorLspTestContext,
) {
cx.set_state(not_reverted_text_with_selections);
cx.set_head_text(base_text);
cx.clear_index_text();
cx.executor().run_until_parked();
let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
@@ -16702,7 +16948,7 @@ fn assert_hunk_revert(
let reverted_hunk_statuses = snapshot
.buffer_snapshot
.diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
.map(|hunk| hunk.status())
.map(|hunk| hunk.status().kind)
.collect::<Vec<_>>();
editor.git_restore(&Default::default(), window, cx);

View File

@@ -19,28 +19,30 @@ use crate::{
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
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,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
Subscription, TextRun, TextStyleRefinement, Window,
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
TextStyleRefinement, Window,
};
use inline_completion::Direction;
use itertools::Itertools;
use language::{
language_settings::{
@@ -54,7 +56,7 @@ use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
use settings::Settings;
use smallvec::{smallvec, SmallVec};
use std::{
@@ -195,7 +197,7 @@ impl EditorElement {
register_action(editor, window, Editor::backspace);
register_action(editor, window, Editor::delete);
register_action(editor, window, Editor::tab);
register_action(editor, window, Editor::tab_prev);
register_action(editor, window, Editor::backtab);
register_action(editor, window, Editor::indent);
register_action(editor, window, Editor::outdent);
register_action(editor, window, Editor::autoindent);
@@ -429,6 +431,13 @@ impl EditorElement {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.organize_imports(action, window, cx) {
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, Editor::restart_language_server);
register_action(editor, window, Editor::show_character_palette);
register_action(editor, window, |editor, action, window, cx| {
@@ -2009,7 +2018,7 @@ impl EditorElement {
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
window: &mut Window,
cx: &mut App,
@@ -2085,7 +2094,7 @@ impl EditorElement {
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
rows_with_hunk_bounds,
display_hunks,
window,
cx,
);
@@ -2103,7 +2112,7 @@ impl EditorElement {
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
@@ -2128,7 +2137,7 @@ impl EditorElement {
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
rows_with_hunk_bounds,
display_hunks,
window,
cx,
);
@@ -2715,7 +2724,10 @@ impl EditorElement {
.shadow_md()
.border_1()
.map(|div| {
let border_color = if is_selected && is_folded {
let border_color = if is_selected
&& is_folded
&& focus_handle.contains_focused(window, cx)
{
colors.border_focused
} else {
colors.border
@@ -4336,7 +4348,9 @@ impl EditorElement {
}
}
fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let is_light = cx.theme().appearance().is_light();
if layout.display_hunks.is_empty() {
return;
}
@@ -4356,7 +4370,7 @@ impl EditorElement {
hunk_bounds,
cx.theme().colors().version_control_modified,
Corners::all(px(0.)),
DiffHunkSecondaryStatus::None,
DiffHunkStatus::modified_none(),
))
}
DisplayDiffHunk::Unfolded {
@@ -4368,19 +4382,19 @@ impl EditorElement {
hunk_hitbox.bounds,
cx.theme().colors().version_control_added,
Corners::all(px(0.)),
status.secondary,
*status,
),
DiffHunkStatusKind::Modified => (
hunk_hitbox.bounds,
cx.theme().colors().version_control_modified,
Corners::all(px(0.)),
status.secondary,
*status,
),
DiffHunkStatusKind::Deleted if !display_row_range.is_empty() => (
hunk_hitbox.bounds,
cx.theme().colors().version_control_deleted,
Corners::all(px(0.)),
status.secondary,
*status,
),
DiffHunkStatusKind::Deleted => (
Bounds::new(
@@ -4392,23 +4406,31 @@ impl EditorElement {
),
cx.theme().colors().version_control_deleted,
Corners::all(1. * line_height),
status.secondary,
*status,
),
}),
};
if let Some((hunk_bounds, background_color, corner_radii, secondary_status)) =
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
hunk_to_paint
{
let background_color = if secondary_status != DiffHunkSecondaryStatus::None {
background_color.opacity(0.3)
} else {
background_color.opacity(1.0)
};
if secondary_status.has_secondary_hunk() {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
// Flatten the background color with the editor color to prevent
// elements below transparent hunks from showing through
let flattened_background_color = cx
.theme()
.colors()
.editor_background
.blend(background_color);
window.paint_quad(quad(
hunk_bounds,
corner_radii,
background_color,
flattened_background_color,
Edges::default(),
transparent_black(),
));
@@ -4536,7 +4558,7 @@ impl EditorElement {
)
});
if show_git_gutter {
Self::paint_diff_hunks(layout, window, cx)
Self::paint_gutter_diff_hunks(layout, window, cx)
}
let highlight_width = 0.275 * layout.position_map.line_height;
@@ -4674,7 +4696,7 @@ impl EditorElement {
.read(cx)
.buffer
.read(cx)
.settings_at(0, cx)
.language_settings(cx)
.show_whitespaces;
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
@@ -5095,9 +5117,15 @@ impl EditorElement {
end_display_row.0 -= 1;
}
let color = match &hunk.status().kind {
DiffHunkStatusKind::Added => theme.status().created,
DiffHunkStatusKind::Modified => theme.status().modified,
DiffHunkStatusKind::Deleted => theme.status().deleted,
DiffHunkStatusKind::Added => {
theme.colors().version_control_added
}
DiffHunkStatusKind::Modified => {
theme.colors().version_control_modified
}
DiffHunkStatusKind::Deleted => {
theme.colors().version_control_deleted
}
};
ColoredRange {
start: start_display_row,
@@ -5661,7 +5689,7 @@ fn prepaint_gutter_button(
gutter_dimensions: &GutterDimensions,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
window: &mut Window,
cx: &mut App,
) -> AnyElement {
@@ -5673,9 +5701,23 @@ fn prepaint_gutter_button(
let indicator_size = button.layout_as_root(available_space, window, cx);
let blame_width = gutter_dimensions.git_blame_entries_width;
let gutter_width = rows_with_hunk_bounds
.get(&row)
.map(|bounds| bounds.size.width);
let gutter_width = display_hunks
.binary_search_by(|(hunk, _)| match hunk {
DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => {
if display_row_range.end <= row {
Ordering::Less
} else if display_row_range.start > row {
Ordering::Greater
} else {
Ordering::Equal
}
}
})
.ok()
.and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
let mut x = left_offset;
@@ -6693,14 +6735,17 @@ impl Element for EditorElement {
.editor
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let is_light = cx.theme().appearance().is_light();
let use_pattern = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
continue;
};
let staged_opacity = 0.10;
let unstaged_opacity = 0.04;
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted => {
@@ -6711,16 +6756,34 @@ impl Element for EditorElement {
continue;
}
};
let background_color =
if diff_status.secondary == DiffHunkSecondaryStatus::None {
background_color.opacity(staged_opacity)
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let staged_background =
solid_background(background_color.opacity(hunk_opacity));
let unstaged_background = if use_pattern {
pattern_slash(
background_color.opacity(hunk_opacity),
window.rem_size().0 * 1.125, // ~18 by default
)
} else {
solid_background(background_color.opacity(if is_light {
0.08
} else {
background_color.opacity(unstaged_opacity)
};
0.04
}))
};
let background = if unstaged {
unstaged_background
} else {
staged_background
};
highlighted_rows
.entry(start_row + DisplayRow(ix as u32))
.or_insert(background_color.into());
.or_insert(background);
}
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
@@ -7170,27 +7233,6 @@ impl Element for EditorElement {
let gutter_settings = EditorSettings::get_global(cx).gutter;
let rows_with_hunk_bounds = display_hunks
.iter()
.filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
.fold(
HashMap::default(),
|mut rows_with_hunk_bounds, (hunk, bounds)| {
match hunk {
DisplayDiffHunk::Folded { display_row } => {
rows_with_hunk_bounds.insert(*display_row, bounds);
}
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => {
for display_row in display_row_range.iter_rows() {
rows_with_hunk_bounds.insert(display_row, bounds);
}
}
}
rows_with_hunk_bounds
},
);
let mut code_actions_indicator = None;
if let Some(newest_selection_head) = newest_selection_head {
let newest_selection_point =
@@ -7240,7 +7282,7 @@ impl Element for EditorElement {
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
&display_hunks,
window,
cx,
);
@@ -7268,7 +7310,7 @@ impl Element for EditorElement {
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
&display_hunks,
&snapshot,
window,
cx,
@@ -8763,65 +8805,62 @@ fn diff_hunk_controls(
.rounded_b_lg()
.bg(cx.theme().colors().editor_background)
.gap_1()
.when(status.secondary == DiffHunkSecondaryStatus::None, |el| {
el.child(
Button::new("unstage", "Unstage")
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Unstage Hunk",
&::git::ToggleStaged,
&focus_handle,
.child(if status.has_secondary_hunk() {
Button::new(("stage", row as u64), "Stage")
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Stage Hunk",
&::git::ToggleStaged,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
true,
&[hunk_range.start..hunk_range.start],
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
false,
&[hunk_range.start..hunk_range.start],
window,
cx,
);
});
}
}),
)
})
.when(status.secondary != DiffHunkSecondaryStatus::None, |el| {
el.child(
Button::new("stage", "Stage")
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Stage Hunk",
&::git::ToggleStaged,
&focus_handle,
);
});
}
})
} else {
Button::new(("unstage", row as u64), "Unstage")
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Unstage Hunk",
&::git::ToggleStaged,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
false,
&[hunk_range.start..hunk_range.start],
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
true,
&[hunk_range.start..hunk_range.start],
window,
cx,
);
});
}
}),
)
);
});
}
})
})
.child(
Button::new("discard", "Restore")
@@ -8875,8 +8914,13 @@ 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_position(&snapshot, position, window, cx);
editor.go_to_hunk_after_or_before_position(
&snapshot,
position,
Direction::Next,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}
@@ -8892,7 +8936,7 @@ fn diff_hunk_controls(
move |window, cx| {
Tooltip::for_action_in(
"Previous Hunk",
&GoToPrevHunk,
&GoToPreviousHunk,
&focus_handle,
window,
cx,
@@ -8906,7 +8950,13 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_position(&snapshot, point, window, cx);
editor.go_to_hunk_after_or_before_position(
&snapshot,
point,
Direction::Prev,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}

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

@@ -581,6 +581,7 @@ impl InlayHintCache {
self.version += 1;
}
self.update_tasks.clear();
self.refresh_task = Task::ready(());
self.hints.clear();
}

View File

@@ -618,11 +618,8 @@ impl Item for Editor {
ItemSettings::get_global(cx)
.file_icons
.then(|| {
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))
path_for_buffer(&self.buffer, 0, true, cx)
.and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
})
.flatten()
.map(Icon::from_path)
@@ -1592,11 +1589,13 @@ 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),
@@ -1609,6 +1608,7 @@ impl SearchableItem for Editor {
}
pub fn active_match_index(
direction: Direction,
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
@@ -1616,7 +1616,7 @@ pub fn active_match_index(
if ranges.is_empty() {
None
} else {
match ranges.binary_search_by(|probe| {
let r = ranges.binary_search_by(|probe| {
if probe.end.cmp(cursor, buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(cursor, buffer).is_gt() {
@@ -1624,8 +1624,15 @@ pub fn active_match_index(
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
});
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)),
},
}
}
}

View File

@@ -185,7 +185,7 @@ impl ProposedChangesEditor {
} else {
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
new_diffs.push(cx.new(|cx| {
let mut diff = BufferDiff::new(branch_buffer.read(cx));
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
let _ = diff.set_base_text(
location.buffer.clone(),
branch_buffer.read(cx).text_snapshot(),

View File

@@ -310,7 +310,7 @@ impl SignatureHelpPopover {
.child(
div().px_4().pb_1().child(
StyledText::new(self.label.clone())
.with_highlights(&self.style, self.highlights.iter().cloned()),
.with_default_highlights(&self.style, self.highlights.iter().cloned()),
),
)
.into_any_element()

View File

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

View File

@@ -275,11 +275,7 @@ async fn run_evaluation(
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,
PathBuf::from("/non/existent/askpass"),
)) as Arc<dyn Fs>;
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {

View File

@@ -339,6 +339,20 @@ async fn test_themes(
let theme_path = extension_path.join(relative_theme_path);
let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?;
log::info!("loaded theme family {}", theme_family.name);
for theme in &theme_family.themes {
if theme
.style
.colors
.deprecated_scrollbar_thumb_background
.is_some()
{
bail!(
r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
theme_name = theme.name
)
}
}
}
Ok(())

View File

@@ -168,11 +168,14 @@ pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Cont
);
workspace.show_notification(notification_id, cx, |cx| {
cx.new(move |_cx| {
MessageNotification::new(format!(
"Do you want to install the recommended '{}' extension for '{}' files?",
extension_id, file_name_or_extension
))
cx.new(move |cx| {
MessageNotification::new(
format!(
"Do you want to install the recommended '{}' extension for '{}' files?",
extension_id, file_name_or_extension
),
cx,
)
.primary_message("Yes, install extension")
.primary_icon(IconName::Check)
.primary_icon_color(Color::Success)

View File

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

View File

@@ -1,5 +1,7 @@
#[cfg(test)]
mod file_finder_tests;
#[cfg(test)]
mod open_path_prompt_tests;
pub mod file_finder_settings;
mod new_path_prompt;
@@ -44,7 +46,7 @@ use workspace::{
Workspace,
};
actions!(file_finder, [SelectPrev, ToggleMenu]);
actions!(file_finder, [SelectPrevious, ToggleMenu]);
impl ModalView for FileFinder {
fn on_before_dismiss(
@@ -199,9 +201,14 @@ impl FileFinder {
}
}
fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {
fn handle_select_prev(
&mut self,
_: &SelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.init_modifiers = Some(window.modifiers());
window.dispatch_action(Box::new(menu::SelectPrev), cx);
window.dispatch_action(Box::new(menu::SelectPrevious), cx);
}
fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) {

View File

@@ -3,7 +3,7 @@ use std::{assert_eq, future::IntoFuture, path::Path, time::Duration};
use super::*;
use editor::Editor;
use gpui::{Entity, TestAppContext, VisualTestContext};
use menu::{Confirm, SelectNext, SelectPrev};
use menu::{Confirm, SelectNext, SelectPrevious};
use project::{RemoveOptions, FS_WATCH_LATENCY};
use serde_json::json;
use util::path;
@@ -2059,7 +2059,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
// Switch to navigating with other shortcuts
// Don't open file on modifiers release
cx.simulate_modifiers_change(Modifiers::control());
cx.dispatch_action(menu::SelectPrev);
cx.dispatch_action(menu::SelectPrevious);
cx.simulate_modifiers_change(Modifiers::none());
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
@@ -2071,7 +2071,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
// Back to navigation with initial shortcut
// Open file on modifiers release
cx.simulate_modifiers_change(Modifiers::secondary_key());
cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
cx.simulate_modifiers_change(Modifiers::none());
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();

View File

@@ -192,7 +192,7 @@ impl Match {
}
}
StyledText::new(text).with_highlights(&window.text_style().clone(), highlights)
StyledText::new(text).with_default_highlights(&window.text_style().clone(), highlights)
}
}

View File

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

View File

@@ -0,0 +1,324 @@
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

@@ -135,6 +135,7 @@ pub trait Fs: Send + Sync {
Arc<dyn Watcher>,
);
fn home_dir(&self) -> Option<PathBuf>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
@@ -248,7 +249,6 @@ impl From<MTime> for proto::Timestamp {
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
}
pub trait FileHandle: Send + Sync + std::fmt::Debug {
@@ -303,12 +303,10 @@ impl RealFs {
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
askpass_path,
}
}
}
@@ -772,7 +770,6 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.askpass_path.to_owned(),
self.git_hosting_provider_registry.clone(),
)))
}
@@ -817,6 +814,10 @@ impl Fs for RealFs {
temp_dir.close()?;
case_sensitive
}
fn home_dir(&self) -> Option<PathBuf> {
Some(paths::home_dir().clone())
}
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
@@ -850,6 +851,7 @@ struct FakeFsState {
metadata_call_count: usize,
read_dir_call_count: usize,
moves: std::collections::HashMap<u64, PathBuf>,
home_dir: Option<PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -1035,6 +1037,7 @@ impl FakeFs {
read_dir_call_count: 0,
metadata_call_count: 0,
moves: Default::default(),
home_dir: None,
}),
});
@@ -1528,6 +1531,10 @@ impl FakeFs {
fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
self.executor.simulate_random_delay()
}
pub fn set_home_dir(&self, home_dir: PathBuf) {
self.state.lock().home_dir = Some(home_dir);
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -2083,6 +2090,10 @@ impl Fs for FakeFs {
fn as_fake(&self) -> Arc<FakeFs> {
self.this.upgrade().unwrap()
}
fn home_dir(&self) -> Option<PathBuf> {
self.state.lock().home_dir.clone()
}
}
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {

View File

@@ -1,5 +1,5 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
sync::atomic::{self, AtomicBool},
};
@@ -50,22 +50,24 @@ 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: MatchCandidate, R, F>(
pub fn match_candidates<C, R, F, T>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
candidates: impl Iterator<Item = T>,
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.has_chars(self.query_char_bag) {
if !candidate.borrow().has_chars(self.query_char_bag) {
continue;
}
@@ -75,7 +77,7 @@ impl<'a> Matcher<'a> {
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
for c in candidate.borrow().to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
}
@@ -98,7 +100,11 @@ impl<'a> Matcher<'a> {
);
if score > 0.0 {
results.push(build_match(&candidate, score, &self.match_positions));
results.push(build_match(
candidate.borrow(),
score,
&self.match_positions,
));
}
}
}

View File

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

View File

@@ -30,11 +30,11 @@ schemars.workspace = true
serde.workspace = true
smol.workspace = true
sum_tree.workspace = true
tempfile.workspace = true
text.workspace = true
time.workspace = true
url.workspace = true
util.workspace = true
tempfile.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -26,6 +26,7 @@ pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
pub static LFS_DIR: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("lfs"));
pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
@@ -56,6 +57,7 @@ actions!(
Pull,
Fetch,
Commit,
ShowCommitEditor,
]
);
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);

View File

@@ -10,11 +10,10 @@ use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
use std::env::temp_dir;
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::os::unix::net::UnixListener;
use std::process::{Command, Stdio};
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;
use std::process::Stdio;
use std::sync::LazyLock;
use std::{
cmp::Ordering,
@@ -64,6 +63,12 @@ pub enum UpstreamTracking {
Tracked(UpstreamTrackingStatus),
}
impl From<UpstreamTrackingStatus> for UpstreamTracking {
fn from(status: UpstreamTrackingStatus) -> Self {
UpstreamTracking::Tracked(status)
}
}
impl UpstreamTracking {
pub fn is_gone(&self) -> bool {
matches!(self, UpstreamTracking::Gone)
@@ -77,6 +82,18 @@ impl UpstreamTracking {
}
}
#[derive(Debug)]
pub struct RemoteCommandOutput {
pub stdout: String,
pub stderr: String,
}
impl RemoteCommandOutput {
pub fn is_empty(&self) -> bool {
self.stdout.is_empty() && self.stderr.is_empty()
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct UpstreamTrackingStatus {
pub ahead: u32,
@@ -182,10 +199,10 @@ pub trait GitRepository: Send + Sync {
branch_name: &str,
upstream_name: &str,
options: Option<PushOptions>,
) -> Result<()>;
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
) -> Result<RemoteCommandOutput>;
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
fn fetch(&self) -> Result<()>;
fn fetch(&self) -> Result<RemoteCommandOutput>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
@@ -203,7 +220,6 @@ impl std::fmt::Debug for dyn GitRepository {
pub struct RealGitRepository {
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
pub askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
@@ -211,13 +227,11 @@ impl RealGitRepository {
pub fn new(
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
askpass_path,
hosting_provider_registry,
}
}
@@ -611,22 +625,30 @@ impl GitRepository for RealGitRepository {
branch_name: &str,
remote_name: &str,
options: Option<PushOptions>,
) -> Result<()> {
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["push", "--quiet"])
.args(["push"])
.args(options.map(|option| match option {
PushOptions::SetUpstream => "--set-upstream",
PushOptions::Force => "--force-with-lease",
}))
.arg(remote_name)
.arg(format!("{}:{}", branch_name, branch_name))
.output()?;
.arg(format!("{}:{}", branch_name, branch_name));
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@@ -634,22 +656,33 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
Ok(())
return Ok(RemoteCommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
}
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["pull"])
.arg(remote_name)
.arg(branch_name)
.output()?;
.arg(branch_name);
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@@ -657,20 +690,31 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
return Ok(RemoteCommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
}
fn fetch(&self) -> Result<()> {
fn fetch(&self) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["fetch", "--quiet", "--all"])
.output()?;
.args(["fetch", "--all"]);
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@@ -678,7 +722,10 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
return Ok(RemoteCommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
}
@@ -725,6 +772,18 @@ impl GitRepository for RealGitRepository {
}
}
#[cfg(not(windows))]
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
let temp_dir = tempfile::Builder::new()
.prefix("zed-git-askpass")
.tempdir()?;
let askpass_script = "#!/bin/sh\necho ''";
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
std::fs::write(&askpass_script_path, askpass_script)?;
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
Ok((askpass_script_path, temp_dir))
}
#[derive(Debug, Clone)]
pub struct FakeGitRepository {
state: Arc<Mutex<FakeGitRepositoryState>>,
@@ -908,15 +967,20 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
fn push(
&self,
_branch: &str,
_remote: &str,
_options: Option<PushOptions>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn fetch(&self) -> Result<()> {
fn fetch(&self) -> Result<RemoteCommandOutput> {
unimplemented!()
}

View File

@@ -20,6 +20,7 @@ test-support = ["multi_buffer/test-support"]
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -29,6 +30,9 @@ git.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
linkify.workspace = true
linkme.workspace = true
log.workspace = true
menu.workspace = true
multi_buffer.workspace = true
panel.workspace = true
@@ -40,6 +44,7 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
strum.workspace = true
theme.workspace = true
time.workspace = true
@@ -52,6 +57,8 @@ zed_actions.workspace = true
windows.workspace = true
[dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }

View File

@@ -1,18 +1,16 @@
use anyhow::{Context as _, Result};
use anyhow::Context as _;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::repository::Branch;
use gpui::{
rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, Window,
};
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectPath};
use std::sync::Arc;
use ui::{
prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TriggerablePopover,
};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@@ -31,35 +29,16 @@ pub fn open(
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
let this = cx.entity();
let style = BranchListStyle::Modal;
cx.spawn_in(window, |_, mut cx| async move {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?;
this.update_in(&mut cx, move |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
let mut list = BranchList::new(project, style, 34., cx);
list._subscription = Some(_subscription);
list.picker = Some(picker);
list
})
})?;
Ok(())
workspace.toggle_modal(window, cx, |window, cx| {
BranchList::new(project, style, 34., window, cx)
})
.detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
}
pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
cx.new(|cx| {
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
list.reload_branches(window, cx);
let list = BranchList::new(project, BranchListStyle::Popover, 15., window, cx);
list.focus_handle(cx).focus(window);
list
})
}
@@ -72,59 +51,54 @@ enum BranchListStyle {
pub struct BranchList {
rem_width: f32,
popover_handle: PopoverMenuHandle<Self>,
default_focus_handle: FocusHandle,
project: Entity<Project>,
style: BranchListStyle,
pub picker: Option<Entity<Picker<BranchListDelegate>>>,
_subscription: Option<Subscription>,
}
impl TriggerablePopover for BranchList {
fn menu_handle(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> PopoverMenuHandle<Self> {
self.popover_handle.clone()
}
pub popover_handle: PopoverMenuHandle<Self>,
pub picker: Entity<Picker<BranchListDelegate>>,
_subscription: Subscription,
}
impl BranchList {
fn new(project: Entity<Project>, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self {
fn new(
project_handle: Entity<Project>,
style: BranchListStyle,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let popover_handle = PopoverMenuHandle::default();
Self {
project,
picker: None,
rem_width,
popover_handle,
default_focus_handle: cx.focus_handle(),
style,
_subscription: None,
}
}
let project = project_handle.read(cx);
let all_branches_request = project
.visible_worktrees(cx)
.next()
.map(|worktree| project.branches(ProjectPath::root_path(worktree.read(cx).id()), cx))
.context("No worktrees found");
fn reload_branches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let project = self.project.clone();
let style = self.style;
cx.spawn_in(window, |this, mut cx| async move {
let delegate = BranchListDelegate::new(project, style, 20, &cx).await?;
let picker =
cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?;
let all_branches = all_branches_request?.await?;
this.update(&mut cx, |branch_list, cx| {
let subscription =
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent));
branch_list.picker = Some(picker);
branch_list._subscription = Some(subscription);
cx.notify();
this.update_in(&mut cx, |this, window, cx| {
this.picker.update(cx, |picker, cx| {
picker.delegate.all_branches = Some(all_branches);
picker.refresh(window, cx);
})
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
let delegate = BranchListDelegate::new(project_handle.clone(), style, 20);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
Self {
picker,
rem_width,
popover_handle,
_subscription,
}
}
}
impl ModalView for BranchList {}
@@ -132,10 +106,7 @@ impl EventEmitter<DismissEvent> for BranchList {}
impl Focusable for BranchList {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker
.as_ref()
.map(|picker| picker.focus_handle(cx))
.unwrap_or_else(|| self.default_focus_handle.clone())
self.picker.focus_handle(cx)
}
}
@@ -143,24 +114,13 @@ impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(self.rem_width))
.map(|parent| match self.picker.as_ref() {
Some(picker) => parent.child(picker.clone()).on_mouse_down_out({
let picker = picker.clone();
cx.listener(move |_, _, window, cx| {
picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
.child(self.picker.clone())
.on_mouse_down_out({
cx.listener(move |this, _, window, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
}),
None => parent.child(
h_flex()
.id("branch-picker-error")
.on_click(
cx.listener(|this, _, window, cx| this.reload_branches(window, cx)),
)
.child("Could not load branches.")
.child("Click to retry"),
),
})
})
}
}
@@ -184,7 +144,7 @@ impl BranchEntry {
pub struct BranchListDelegate {
matches: Vec<BranchEntry>,
all_branches: Vec<Branch>,
all_branches: Option<Vec<Branch>>,
project: Entity<Project>,
style: BranchListStyle,
selected_index: usize,
@@ -194,33 +154,20 @@ pub struct BranchListDelegate {
}
impl BranchListDelegate {
async fn new(
fn new(
project: Entity<Project>,
style: BranchListStyle,
branch_name_trailoff_after: usize,
cx: &AsyncApp,
) -> Result<Self> {
let all_branches_request = cx.update(|cx| {
let project = project.read(cx);
let first_worktree = project
.visible_worktrees(cx)
.next()
.context("No worktrees found")?;
let project_path = ProjectPath::root_path(first_worktree.read(cx).id());
anyhow::Ok(project.branches(project_path, cx))
})??;
let all_branches = all_branches_request.await?;
Ok(Self {
) -> Self {
Self {
matches: vec![],
project,
style,
all_branches,
all_branches: None,
selected_index: 0,
last_query: Default::default(),
branch_name_trailoff_after,
})
}
}
pub fn branch_count(&self) -> usize {
@@ -261,32 +208,31 @@ impl PickerDelegate for BranchListDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(mut all_branches) = self.all_branches.clone() else {
return Task::ready(());
};
cx.spawn_in(window, move |picker, mut cx| async move {
let candidates = picker.update(&mut cx, |picker, _| {
const RECENT_BRANCHES_COUNT: usize = 10;
let mut branches = picker.delegate.all_branches.clone();
if query.is_empty() {
if branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.priority_key().cmp(&lhs.priority_key())
});
branches.truncate(RECENT_BRANCHES_COUNT);
}
branches.sort_unstable_by(|lhs, rhs| {
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
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);
}
branches
.into_iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<StringMatchCandidate>>()
});
let Some(candidates) = candidates.log_err() else {
return;
};
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
.into_iter()

View File

@@ -2,10 +2,10 @@
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use git::Commit;
use git::{Commit, ShowCommitEditor};
use panel::{panel_button, panel_editor_style, panel_filled_button};
use project::Project;
use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
use editor::{Editor, EditorElement};
use gpui::*;
@@ -110,28 +110,14 @@ struct RestoreDock {
impl CommitModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
workspace.register_action(|workspace, _: &Commit, window, cx| {
workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| {
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
return;
};
let (can_commit, conflict) = git_panel.update(cx, |git_panel, _cx| {
let can_commit = git_panel.can_commit();
let conflict = git_panel.has_unstaged_conflicts();
(can_commit, conflict)
git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
});
if !can_commit {
let message = if conflict {
"There are still conflicts. You must stage these before committing."
} else {
"No changes to commit."
};
let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
cx.spawn(|_, _| async move {
prompt.await.ok();
})
.detach();
}
let dock = workspace.dock_at_position(git_panel.position(window, cx));
let is_open = dock.read(cx).is_open();
@@ -159,30 +145,30 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_message = panel.suggest_commit_message();
let suggested_commit_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
let buffer = git_panel.commit_message_buffer(cx).clone();
let panel_editor = git_panel.commit_editor.clone();
let project = git_panel.project.clone();
cx.new(|cx| commit_message_editor(buffer, None, project.clone(), false, window, cx))
cx.new(|cx| {
let mut editor =
commit_message_editor(buffer, None, project.clone(), false, window, cx);
editor.sync_selections(panel_editor, cx).detach();
editor
})
});
let commit_message = commit_editor.read(cx).text(cx);
if let Some(suggested_message) = suggested_message {
if let Some(suggested_commit_message) = suggested_commit_message {
if commit_message.is_empty() {
commit_editor.update(cx, |editor, cx| {
editor.set_text(suggested_message, window, cx);
editor.select_all(&Default::default(), window, cx);
editor.set_placeholder_text(suggested_commit_message, cx);
});
} else {
if commit_message.as_str().trim() == suggested_message.trim() {
commit_editor.update(cx, |editor, cx| {
// select the message to make it easy to delete
editor.select_all(&Default::default(), window, cx);
});
}
}
}
@@ -246,7 +232,7 @@ impl CommitModal {
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let (branch, tooltip, commit_label, co_authors) =
let (branch, can_commit, tooltip, commit_label, co_authors) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
@@ -258,18 +244,10 @@ impl CommitModal {
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
} else {
"Commit changes to tracked files"
};
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit All"
};
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
(branch, can_commit, tooltip, title, co_authors)
});
let branch_picker_button = panel_button(branch)
@@ -287,12 +265,20 @@ impl CommitModal {
}))
.style(ButtonStyle::Transparent);
let branch_picker = PopoverButton::new(
self.branch_list.clone(),
Corner::BottomLeft,
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
);
let branch_picker = PopoverMenu::new("popover-button")
.menu({
let branch_list = self.branch_list.clone();
move |_window, _cx| Some(branch_list.clone())
})
.trigger_with_tooltip(
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
)
.anchor(Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
});
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
@@ -304,9 +290,8 @@ impl CommitModal {
None
};
let (panel_editor_focus_handle, can_commit) = git_panel.update(cx, |git_panel, cx| {
(git_panel.editor_focus_handle(cx), git_panel.can_commit())
});
let panel_editor_focus_handle =
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
let commit_button = panel_filled_button(commit_label)
.tooltip(move |window, cx| {
@@ -328,12 +313,7 @@ impl CommitModal {
.w_full()
.h(px(self.properties.footer_height))
.gap_1()
.child(
h_flex()
.gap_1()
.child(branch_picker.render(window, cx))
.children(co_authors),
)
.child(h_flex().gap_1().child(branch_picker).children(co_authors))
.child(div().flex_1())
.child(
h_flex()
@@ -350,6 +330,7 @@ impl CommitModal {
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
self.git_panel
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
@@ -373,7 +354,7 @@ impl Render for CommitModal {
.on_action(
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
this.branch_list.update(cx, |branch_list, cx| {
branch_list.menu_handle(window, cx).toggle(window, cx);
branch_list.popover_handle.toggle(window, cx);
})
}),
)

File diff suppressed because it is too large Load Diff

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