Compare commits

..

75 Commits

Author SHA1 Message Date
Ben Kunkle
a90f80725f settings: Enable JSX tag auto-close by default (#26481)
Based on conversation with @maxbrunsfeld. Enabling Tag auto closing by
default so that it is discoverable for new and existing users

Release Notes:

- Made it so JSX tag auto-closing is automatically enabled in supported
languages
2025-03-11 21:01:41 +00:00
Marshall Bowers
4e6c37d23b assistant2: Add tool selector (#26480)
This PR adds a tool selector to Assistant 2 to facilitate customizing
the tools that the model sees:

<img width="1297" alt="Screenshot 2025-03-11 at 4 25 31 PM"
src="https://github.com/user-attachments/assets/7a656343-83bc-4546-9430-6a5f7ff1fd08"
/>

Release Notes:

- N/A
2025-03-11 20:50:18 +00:00
Peter Tripp
0cf6259fec Make nano save (ctrl-o) work by-default in terminal (linux) (#26479)
Closes: https://github.com/zed-industries/zed/issues/15770

Release Notes:

- Make nano save (`ctrl-o`) work by-default in terminal (linux)
2025-03-11 20:29:02 +00:00
Kirill Bulatov
5cb5e92185 Bump aws-lc-rs to fix Windows release builds (#26477)
Closes https://github.com/zed-industries/zed/discussions/24816

https://github.com/aws/aws-lc-rs/releases/tag/v1.12.6 release includes a
fix for https://github.com/aws/aws-lc-rs/issues/707

Release Notes:

- N/A
2025-03-11 19:43:14 +00:00
Marshall Bowers
da61a28839 assistant_tool: Fix inaccurate parameter name (#26473)
This PR fixes an inaccurate parameter name in the
`ToolWorkingSet::insert` method.

Release Notes:

- N/A
2025-03-11 19:15:03 +00:00
Marshall Bowers
efdb769f9b terraform: Extract to zed-extensions/terraform repository (#26475)
This PR extracts the Terraform extension to the
[zed-extensions/terraform](https://github.com/zed-extensions/terraform)
repository.

Release Notes:

- N/A
2025-03-11 19:10:51 +00:00
Marshall Bowers
9cce5a650e assistant_tool: Add a source to the Tool trait (#26471)
This PR adds a `source` method to the `Tool` trait.

This will allow us to track where a tool is coming from.

Release Notes:

- N/A
2025-03-11 19:10:48 +00:00
Piotr Osiewicz
2021ca5bff terraform: Do not add each string constraint to the outline (#26453)
Closes #26336

Release Notes:

- N/A
2025-03-11 19:46:18 +01:00
Peter Tripp
1771250b04 Add 'Open Remote...' to File Menu (#26288)
Added some spacers while I was at it.

Release Notes:

- Added 'Open Remote...' to File menu
2025-03-11 14:18:13 +00:00
张小白
18259c0fd4 chore: Bump windows crate version (#26455)
Closes #ISSUE

Release Notes:

- N/A
2025-03-11 21:14:36 +08:00
Smit Barmase
41ddd1cc97 editor: Fix text selection not visible on text background (#26454)
Closes #25014

Previously, we painted in the order: highlights -> text background ->
text -> etc. This caused text selection to be invisible when the text
had a background.

This PR changes the painting order to: text background -> highlights ->
text -> etc.

Before:


https://github.com/user-attachments/assets/5d9647c4-3ab2-4960-b6b9-e399882a0c50

After:


https://github.com/user-attachments/assets/c699f5b9-4077-45f8-85e5-86c89130eb71

Release Notes:

- Fixed an issue where text selection was not visible on top of a text
background in the editor.
2025-03-11 18:43:11 +05:30
Smit Barmase
e175878008 macOS: Remove multi-keystroke rendering in title of menu item (#26448)
Closes #25483

Currently, macOS doesn't support showing multi-keystroke shortcuts in
menu items. We can use an attributed string to differentiate them, but
that breaks consistency with traditional shortcuts.

This PR removes the hack of concatenating the multi-keystroke shortcut
to the title, as it looked a bit janky.

Release Notes:

- N/A
2025-03-11 18:42:02 +05:30
张小白
1cfbfc199c windows: Fix tests (#26450)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-11 11:43:24 +00:00
张小白
f59f2caf7e Fix tests on Windows (#26449)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-03-11 11:17:48 +00:00
Agus Zubiaga
401342c6ec assistant: Display edits from scripts in panel (#26441)
https://github.com/user-attachments/assets/a486ff2a-4aa1-4c0d-be6c-1dea2a8d60c8
 
- [x] Track buffer changes in `ScriptingSession`
- [x] Show edited files in thread

Reviewing diffs and displaying line counts will be part of an upcoming
PR.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-03-11 10:12:52 +00:00
Cole Miller
0df1e4a489 Address out-of-bounds panic in inline completion button (#26394)
Closes #26350

Release Notes:

- Git Beta: Fixed a panic that could occur when using the project diff
2025-03-11 03:04:56 -04:00
Cole Miller
9bd3e156f5 Fix enter binding in git panel's commit editor on Linux (#26427)
Closes #26110 

Release Notes:

- Git Beta: fixed being unable to enter newline in the git panel's
commit editor on Linux
2025-03-11 00:26:13 -04:00
Conrad Irwin
42c655751b Show a disabled stage all button for no entries (#26436)
Closes #ISSUE

Release Notes:

- N/A
2025-03-10 22:25:33 -06:00
Conrad Irwin
ff1d78df3b Go back to "create branch" in the list (#26433)
Closes #ISSUE

Release Notes:

- N/A
2025-03-11 04:24:52 +00:00
Conrad Irwin
c2e4fdf63d Git commit modal branch list (#26417)
Closes #26273

Release Notes:

- git: Fixes opening the branch selector in the commit modal with
cmd-option-b
- git: Truncates the branch selector in the commit modal
2025-03-10 22:10:52 -06:00
Agus Zubiaga
bf11b888c3 scripting tool: Use project buffers in io.open (#26425)
This PR makes `io.open` use our own implementation again, but instead of
the real filesystem, it will now use the project's to check file
metadata and perform read and writes using project buffers.

This also cleans up the `io.open` implementation by splitting it into
multiple methods, adds tests for various File I/O patterns, and fixes a
few bugs in read formats.

Release Notes:

- N/A
2025-03-11 00:52:16 -03:00
Angelk90
d562f58e76 git_ui: Show more information in the branch picker (#25359)
Final product:

![CleanShot 2025-02-26 at 9  08
17@2x](https://github.com/user-attachments/assets/e5db1932-b2c6-4b32-ab67-ef0a0d19f022)

Release Notes:

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

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-10 21:05:29 -06:00
Conrad Irwin
94e4aa626d Use current upstream for permalink to line (#26398)
Release Notes:

- git: Copy permalink to line now uses the upstream of the current
branch instead of "origin"
2025-03-10 20:53:46 -06:00
lydiandy
8ceba89d81 ui: Fix error code in button comment (#26423)
Closes #ISSUE

Release Notes:
ui: Fix error code in button comment.

- N/A *or* Added/Fixed/Improved ...
2025-03-11 02:15:39 +00:00
Conrad Irwin
c37d6d5fed Unwind deprecated permalinks code (#26395)
Release Notes:

- N/A
2025-03-10 19:57:10 -06:00
Max Brunsfeld
1a3597d726 Fix race conditions in updating buffer diffs on git changes (#26409)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-03-10 16:52:18 -07:00
Kirill Bulatov
c747cccde3 Revert "Return back a proper resolved value (#26406)" (#26419)
This reverts commit 1f8b14f4f1.

Release Notes:

- N/A
2025-03-10 23:33:41 +00:00
Kirill Bulatov
d81e7683ea Use proper order of Completion::Source field to have sane default (#26416)
Follow-up of https://github.com/zed-industries/zed/pull/26300

Release Notes:

- N/A
2025-03-10 18:58:21 -04:00
edwloef
8b29ee6033 Add variable.special color to Gruvbox themes (#26271)
This adds the `variable.special` color to the Gruvbox family of themes,
which colors special variables (in Rust's case `self`) differently than
normal ones. The colors were taken from the old Gruvbox `variable`
highlighting (see https://github.com/zed-industries/zed/pull/25464).

before:

![image](https://github.com/user-attachments/assets/3f329ac0-fbdf-480c-9074-5db99591f4e1)

![image](https://github.com/user-attachments/assets/43efdddd-7daf-440f-8c11-d6279330912a)

after:

![image](https://github.com/user-attachments/assets/052c05b8-55c5-495a-a9cc-a5f73aa5aa00)

![image](https://github.com/user-attachments/assets/f598b75f-8d2d-4710-b804-9282de9f8d15)

fixes half of https://github.com/zed-industries/zed/issues/26206. Since
I don't use the Ayu themes I'd prefer someone who knows what looks good
there does those changes.

Release Notes:

- Gruvbox themes: Added a color for `@variable.special` syntax
highlights.
2025-03-10 18:43:18 -04:00
Conrad Irwin
96a75e08af Fix panic opening branch picker in commit modal (#26407)
Closes #ISSUE

Release Notes:

- N/A
2025-03-10 16:36:23 -06:00
Marshall Bowers
06cbff6714 assistant2: Remove excess padding around scripting tool inputs (#26412)
This PR removes some excess padding around the rendered scripting tool
inputs.

Release Notes:

- N/A
2025-03-10 22:29:50 +00:00
Marshall Bowers
ce05813e7c assistant2: Render scripting tool inputs when opening past threads (#26408)
This PR makes it so we render the scripting tool inputs to Markdown when
opening past threads.

Release Notes:

- N/A
2025-03-10 22:11:36 +00:00
Conrad Irwin
4d1d8d6d78 Git commit modal command (#26405)
Fix KeyBinding::for_action() to use the active focus handle instead of
what was
rendered last.

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

Release Notes:

- git: Fixed escape tooltip in commit modal
2025-03-10 16:10:53 -06:00
Kirill Bulatov
1f8b14f4f1 Return back a proper resolved value (#26406)
Follow-up of https://github.com/zed-industries/zed/pull/26300


https://github.com/zed-industries/zed/pull/26300/files#diff-a3da3181e4ab4f73aa1697d7b6dc0caa0c17b2a187fb83b076dfc0234ec91f54L16900
changed the snippets' `resolved` value but it should have not.

Release Notes:

- N/A
2025-03-10 23:48:04 +02:00
Marshall Bowers
082cc6184c assistant2: Persist scripting tool uses in saved threads (#26404)
This PR makes it so the scripting tool uses are persisted to and
restored from saved threads.

Release Notes:

- N/A
2025-03-10 21:42:23 +00:00
Smit Barmase
6cfc4dc857 gpui: Fix transparent titlebar in fullscreen mode on macOS (#26403)
Closes #23735

This PR fixes an issue where Zed shows a transparent title bar in
fullscreen mode on macOS instead of the default gray one.

When switching to fullscreen mode, we change the title bar appearance to
opaque. When exiting fullscreen mode, we check the existing
`appears_transparent` flag that we pass to gpui to decide whether to
change the title bar back to transparent or not.

Note: Regardless of the `appears_transparent` flag, gpui should always
show an opaque title bar in fullscreen mode to prevent a broken
appearance, as macOS always displays the title bar in fullscreen mode
upon mouse interaction.


https://github.com/user-attachments/assets/211fb185-239b-454e-ac7f-b93b25d33805

Release Notes:

- Fixed issue where Zed showed transparent titlebar in fullscreen mode
on macOS.
2025-03-11 03:02:52 +05:30
Ben Kunkle
b9c48685e8 terminal: Support trailing :description or error message after file path (#26401)
Closes #25086

Release Notes:

- Fixed a bug where file paths in the built in terminal of the format
`path/to/file.ext:row:col:description or error message` would not be
correctly identified as file paths due to the colon & additional text at
the end
2025-03-10 16:20:48 -05:00
Marshall Bowers
570c396e84 assistant2: Remove unneeded pub on field (#26399)
This PR removes an unneeded `pub` on a field in the `ContextStrip`, as
it was never accessed externally.

Release Notes:

- N/A
2025-03-10 20:55:53 +00:00
Ben Kunkle
5fd034e604 docs: Add documentation for using debuggers with Zed (#26391)
Just some basic documentation for using debuggers in Zed development.
Goes over configuring cargo to include full debug info, attaching to an
instance of Zed, and using a debugger to debug panics and crashes

Release Notes:

- N/A
2025-03-10 15:51:51 -05:00
Cole Miller
63dab5f891 Add a missing notify when updating the project diff (#26396)
Closes #ISSUE

Release Notes:

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

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-10 16:47:35 -04:00
Marshall Bowers
a2d6df3ed6 scripting_tool: Fix formatting of tool description (#26397)
This PR fixes the formatting of the scripting tool description, as it
had acquired some strange whitespaces.

Release Notes:

- N/A
2025-03-10 20:35:02 +00:00
Mikayla Maki
30e86ac939 Add a "secondary" meta key to GPUI keystroke parsing (#26390)
"secondary" means "cmd" on macOS and "ctrl" on not macOS.

Release Notes:

- Added a "secondary" meta key to the zed keystroke parser, which maps
to 'cmd' on macOS and 'ctrl' off of macOS
2025-03-10 13:32:13 -07:00
Nate Butler
976fc3ee97 git_ui: Design Polish (#26361)
Polish PR

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

### Previous message truncation:

Before:

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

After:

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

### Make branch icon match when menu is disabled

Before:

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

After:

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

Release Notes:

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

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-10 13:19:02 -07:00
Conrad Irwin
63091459d8 Allow too many arguments (#26375)
This is nearly half of our #allows, and seems like something we happily
break whenever we need

Release Notes:

- N/A
2025-03-10 13:38:30 -06:00
Conrad Irwin
659fae70f8 Remove GitUiFeatureFlag and enable panel unconditionally (#26386)
Release Notes:

- git: Enable for everyone
2025-03-10 13:38:07 -06:00
Marshall Bowers
02e970192f assistant2: Improve Lua script rendering (#26389)
This PR improves the rendering of Lua scripts provided to the scripting
tool.

We now render them in code blocks with syntax highlighting:

<img width="1297" alt="Screenshot 2025-03-10 at 2 40 51 PM"
src="https://github.com/user-attachments/assets/def65b5c-86a8-490f-aaa5-5cc1687fe01e"
/>

Release Notes:

- N/A
2025-03-10 18:54:03 +00:00
Julia Ryan
5ecc67f2ef Remove --frozen flag for cargo-about (#26385)
This was added to support the nix build but accidentally broke our
bundling. I'll try to re-add it in a way that works for both in the
future.

Release Notes:

- N/A
2025-03-10 18:35:59 +00:00
Conrad Irwin
73dfb10c16 Scroll project diff into view always (#26379)
Closes #ISSUE

Release Notes:

- N/A
2025-03-10 12:28:07 -06:00
Marshall Bowers
e513e81046 assistant2: Decouple scripting tool from the Tool trait (#26382)
This PR decouples the scripting tool from the `Tool` trait while still
allowing it to be used as a tool from the model's perspective.

This will allow us to evolve the scripting tool as more of a first-class
citizen while still retaining the ability to have the model call it as a
regular tool.

Release Notes:

- N/A
2025-03-10 17:57:03 +00:00
Agus Zubiaga
2fc4dec58f assistant: Use tool interface for scripts (#26377)
We decided to expose scripting as tools again. We are aware of the UX
downsides of doing so, but we want to focus on getting it working well
first, and the model seems to make better use of it as an actual tool.

In the future, the tools API might support streaming. If it doesn't and
we need to ship, we can consider reverting this.

Release Notes:

- N/A
2025-03-10 13:59:31 -03:00
Conrad Irwin
3891381d3e Git keyboard shortcuts (#26374)
Closes #26040

Release Notes:

- git: Add keyboard shortcuts (when the panel is open) for fetch `ctrl-g
ctrl-g`, pull `ctrl-g down`, push `ctrl-g up`, force-push `ctrl-g
shift-up`, open diff `ctrl-g d`
2025-03-10 10:46:53 -06:00
Cole Miller
b91e929086 git: Pass project environment to git binary invocations (#26301)
Closes #26213 

Release Notes:

- Git Beta: pass down environment variables from project to git
operations
2025-03-10 12:12:46 -04:00
Cole Miller
013a646799 git_ui: Branch picker improvements (#26287)
- Truncate branch names based on the width of the picker
- Use a footer for "Create branch" instead of a picker entry

Still to do:

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

Release Notes:

- Git Beta: Removed limitation that made it impossible to create a
branch from the branch picker when it too closely resembled an existing
branch name
2025-03-10 11:39:01 -04:00
Marshall Bowers
ed52e759d7 docs: Fix language links (#26368)
This PR fixes some language links in the docs.

The Shell Script page wasn't being linked from `SUMMARY.md`, so no page
was being generated.

There were also some differences in the language lists in the sidebar
and on the top-level languages page.

Release Notes:

- N/A
2025-03-10 15:22:51 +00:00
Richard Feldman
6da099a9d7 Unsandbox Lua scripts (#26365)
Per a conversation with @nathansobo, have the Lua scripts run
unsandboxed for now (while this feature is behind the staff feature
flag).

Release Notes:

- N/A
2025-03-10 11:04:37 -04:00
Smit Barmase
5f159bc95e go_to_line: Fix goto line + mouse click jumps to previous scroll position (#26362)
Closes #20658

Now, when the "Go to Line" palette is open:  
- Clicking on the editor will dismiss the palette without changing the
scroll position. (PR change)
- Pressing Enter will jump to the line number entered in the palette.
(Unchanged)
- Pressing Escape will jump back to the previous cursor location.
(Unchanged)

Release Notes:

- Fixed an issue where clicking the editor with the mouse while the "Go
to Line" palette is open would cause it to jump to the previous scroll
position.
2025-03-10 20:33:07 +05:30
Marshall Bowers
a4462577bf Sort Cargo.tomls (#26367)
This PR sorts some `Cargo.toml`s that had become unsorted.

Release Notes:

- N/A
2025-03-10 14:48:21 +00:00
João Marcos
c147b58558 Remove redundant checks in do_stage_or_unstage_and_next (#26364)
Release Notes:

- N/A
2025-03-10 14:23:17 +00:00
Devzeth
84fe1bfe9b Recognize ixx as part of the cpp suffix (#26333)
Adds "ixx" as path suffix to be recognized for c++. 

> ixx documentation
https://learn.microsoft.com/en-us/cpp/cpp/modules-cpp?view=msvc-

I've also added it to the icon file. 

Release Notes:

- N/A
2025-03-10 09:10:29 -05:00
Danilo Leal
657d7a911d Add logo for wgsl (WebGPU Shading Language) (#26360)
Was dabbling on the shaders these past few days and felt like we could
have the WGSL logo. This is based on the logo found on the GPU Web
repository: https://github.com/gpuweb/gpuweb/tree/main/logo

Release Notes:

- N/A
2025-03-10 09:44:19 -03:00
Danilo Leal
ee05cc3ad9 Add a line numbers toggle to the editor controls menu (#26318)
Closes https://github.com/zed-industries/zed/issues/26305

<img
src="https://github.com/user-attachments/assets/795029ad-128a-471f-9adf-c0ef26319bbf"
width="400px" />

Release Notes:

- N/A
2025-03-10 09:28:46 -03:00
Smit Barmase
5ed144f9d2 macOS: Add support for external file managers to open directory in Zed (#26357)
Closes #25421

This PR adds support for external file managers to show Zed as an option
in the "Open With" context menu for directories on macOS.

<img width="350" alt="image"
src="https://github.com/user-attachments/assets/c52acd48-73c4-47be-8683-6950e0371b73"
/>


Release Notes:

- Added support for opening folders in Zed from third-party macOS file
managers like Path Finder and Super Charge through their "Open With"
menu.
2025-03-10 15:21:39 +05:30
Julia Ryan
2a862b3c54 nix: Disable checks and remove crane workaround (#26356)
The checkPhase was failing for me in darwin so I turned it off. I think
eventually we'll want to use a separate derivation for tests (which
crane has a helper for).

Crane also solved our issue with spaces in paths so I bumped the flake
to pick up that fix and removed our workaround: ipetkov/crane#808.

Release Notes:

- N/A
2025-03-10 02:00:57 -07:00
Julia Ryan
4a7c84f490 Fix nix build (#26270)
This PR includes lots of small fixes to get our `build.nix` and
`shell.nix` back to a working state.

I've tested this by running `cargo run` (inside the devshell) and `nix
run` on x86 nixos and arm64 darwin machines. I'd appreciate it if others
could test building inside the devshell to double-check that it's not
just working because I happen to have some system-level packages
installed, as well as seeing if it works on other platforms (non-nixos
linux, arm linux, x86 darwin).

I couldn't get the full test suite (`cargo nextest run --workspace`)
passing in the devshell on darwin, but they _are_ all passing on nixos.
nixpkgs [disables some of our
tests](92d11f06d5/pkgs/by-name/ze/zed-editor/package.nix (L226-L234))
that apparently fail or are flakey on hydra, but they don't know why.
I'm going to punt on debugging those for now, especially given that they
seem to be working for me. I'm also unsure of whether we actually want
the nix checkPhase to run the full test suite (it's currently not
passing `--workspace`) given that we have separate CI that should
enforce that those pass on all PRs.

Here's an overview of the changes made:
- Fix our `generate-licenses` script
- Relaxes the `cargo-about` version requirement slightly so it doesn't
try to install an older binary when the nixpkgs one is newer than our
requirement
- Add a workaround for [this cargo-about
issue](https://github.com/zed-industries/zed/issues/19971) obviating the
need for the patching done in the nixpkgs package
- Set the new `--frozen` flag to avoid network access/mutating the
lockfile
- Use dynamic webrtc lib from nixpkgs, and fixes up the build script in
webrtc-sys that hardcodes it to be statically linked.
- Use `inputsFrom` in `shell.nix` and avoid duplicating everything from
`build.nix`
- Add a temporary workaround for an [upstream crane
bug](https://github.com/ipetkov/crane/issues/808).
- Fix shebangs in our `script` dir to not hard-code `/bin/bash`

There are still a bunch of issues that aren't resolved here, I'll make a
tracking issue for those and try to land this first just to get back to
an unbroken state. Eventually among other things I'd like to use a
`libgit2` from `staticPkgs` and musl cross compilation to build the
remote server under nix, and then add that as a separate flake output
and include it in the shell's `inputsFrom` list.

Thanks @niklaskorz, @GaetanLepage, @bbigras and all the other nixpkgs
maintainers that have kept the `zed-editor` package working and up to
date! I seriously considered just making our flake `overrideAttrs` the
package in nixpkgs given how well maintained it is.

Thanks @WeetHet for your volunteer maintinance of this flake. I
referenced #24953 while working on these fixes, and I'd love to
collaborate on adding some of those pieces like treefmt and a github
action. If you're interested I'd really appreciate some help debugging
why crane's `buildDepsOnly` isn't working for us. I'm assuming it'd make
our `nix build` times go way down from the improved dep caching if we
could get it working.

Thanks @rrbutani for all the help on this PR 💙.

Release Notes:

- N/A

---------

Co-authored-by: Rahul Butani <rrbutani@users.noreply.github.com>
Co-authored-by: Rahul Butani <rr.butani@gmail.com>
2025-03-10 01:06:11 -07:00
Mikayla Maki
230e2e4107 Restore git panel header (#26354)
Let's play around with it. This should not be added to tomorrow's
preview.

Release Notes:

- Git Beta: Added a panel header with an open diff and stage/unstage all
buttons.
2025-03-10 07:08:10 +00:00
Richard Hao
d732b8ba0f git: Disable commit message generation when commit not possible (#26329)
## Issue:

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


## After Fixed:

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


## Release Notes:

- Fixed: Disable commit message generation when commit is not possible
2025-03-09 23:45:25 -07:00
Michael Sloan
7c3eecc9c7 Add support for querying file outline in assistant script (#26351)
Release Notes:

- N/A
2025-03-10 05:26:17 +00:00
Max Brunsfeld
fff37ab823 Follow-up fixes for recent multi buffer optimizations (#26345)
I realized that the optimization broke multi buffer syncing after buffer
reparses.

Release Notes:

- N/A
2025-03-09 22:15:38 -07:00
Kirill Bulatov
8a7a78fafb Avoid modifying the LSP message before resolving it (#26347)
Closes https://github.com/zed-industries/zed/issues/21277

To the left is current Zed, right is the improved version.
3rd message, from Zed, to resolve the item, does not have `textEdit` on
the right side, and has one on the left.
Seems to not influence the end result though, but at least Zed behaves
more appropriate now.

<img width="1727" alt="image"
src="https://github.com/user-attachments/assets/ca1236fd-9ce2-41ba-88fe-1f3178cdcbde"
/>


Instead of modifying the original LSP completion item, store completion
list defaults and apply them when the item is requested (except `data`
defaults, needed for resolve).

Now, the only place that can modify the completion items is this method,
and Python impl seems to be the one doing it:


ca9c3af56f/crates/languages/src/python.rs (L182-L204)

Seems ok to leave untouched for now.

Release Notes:

- Fixed LSP completion items modified before resolve request
2025-03-10 00:12:53 +02:00
Ben Kunkle
6de3ac3e17 Revert "Highlight super and this as keywords in JS/TS/TSX" (#26342)
Reverts zed-industries/zed#25135

This approach was not the best as explained in the response to the
original PR. Likely, the better approach is to create a newer specific
scope for these kinds of variables under the `@variable` prefix so that
themes can control these pseudo-keywords specifically
2025-03-09 16:11:37 +00:00
Smit Barmase
5aae3bdc69 copilot: Fix missing sign-out button when Zed is the edit prediction provider (#26340)
Closes #25884

Added a sign-out button for Copilot in Assistant settings, allowing
sign-out even when copilot is disabled.

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/43fc97ad-f73c-49e1-a7b6-a3910434d661"
/>



Release Notes:

- Added a sign-out button for Copilot in Assistant settings.
2025-03-09 21:39:14 +05:30
Agus Zubiaga
e298301b40 assistant: Make scripting a first-class concept instead of a tool (#26338)
This PR makes refactors the scripting functionality to be a first-class
concept of the assistant instead of a generic tool, which will allow us
to build a more customized experience.

- The tool prompt has been slightly tweaked and is now included as a
system message in all conversations. I'm getting decent results, but now
that it isn't in the tools framework, it will probably require more
refining.

- The model will now include an `<eval ...>` tag at the end of the
message with the script. We parse this tag incrementally as it streams
in so that we can indicate that we are generating a script before we see
the closing `</eval>` tag. Later, this will help us interpret the script
as it arrives also.

- Threads now hold a `ScriptSession` entity which manages the state of
all scripts (from parsing to exited) in a centralized way, and will
later collect all script operations so they can be displayed in the UI.

- `script_tool` has been renamed to `assistant_scripting` 

- Script source now opens in a regular read-only buffer  

Note: We still need to handle persistence properly

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-09 09:01:49 +00:00
brian tan
ed6bf7f161 diagnostics: Fix losing focus when activating from diagnostics view (#25517)
Closes #25509

Changes:
- If active item is already diagnostics, don't try to focus it again.

Instead of not focusing, should it just not activate instead? Something
like:

            if !workspace
                .active_item(cx)
                .map(|item| item.item_id() == existing.item_id())
                .unwrap_or(false)
            {
workspace.activate_item(&existing, true, true, window, cx);
            }


Release Notes:

- N/A
2025-03-08 22:17:20 +00:00
Smit Barmase
f14d6670ba copilot: Fix onboarding into Copilot requires Zed restart (#26330)
Closes #25594

This PR fixes an issue where signing into Copilot required restarting
Zed.

Copilot depends on an OAuth token that comes from either `hosts.json` or
`apps.json`. Initially, both files don't exist. If neither file is
found, we fallback to watching `hosts.json` for updates. However, if the
auth process creates `apps.json`, we won't receive updates from it,
causing the UI to remain outdated.

This PR fixes that by watching the parent `github-copilot` directory
instead, which will always contain one of those files along with an
additional version file.

I have tested this on macOS and Linux Wayland.

Release Notes:

- Fixed an issue where signing into Copilot required restarting Zed.
2025-03-09 03:19:09 +05:30
Joseph T. Lyons
22d9b5d8ca Update key binding documentation (#26321)
Release Notes:

- N/A
2025-03-08 01:03:52 -05:00
208 changed files with 4444 additions and 4469 deletions

160
Cargo.lock generated
View File

@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.25.1-dev"
source = "git+https://github.com/zed-industries/alacritty.git?rev=03c2907b44b4189aac5fdeaea331f5aab5c7072e#03c2907b44b4189aac5fdeaea331f5aab5c7072e"
source = "git+https://github.com/zed-industries/alacritty.git?branch=add-hush-login-flag#828457c9ff1f7ea0a0469337cc8a37ee3a1b0590"
dependencies = [
"base64 0.22.1",
"bitflags 2.8.0",
@@ -490,6 +490,7 @@ dependencies = [
"proto",
"rand 0.8.5",
"rope",
"scripting_tool",
"serde",
"serde_json",
"settings",
@@ -1233,27 +1234,25 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.12.2"
version = "1.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01"
dependencies = [
"aws-lc-sys",
"paste",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.25.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491"
checksum = "6bbe221bbf523b625a4dd8585c7f38166e31167ec2ca98051dbcb4c3b6e825d2"
dependencies = [
"bindgen 0.69.5",
"cc",
"cmake",
"dunce",
"fs_extra",
"paste",
]
[[package]]
@@ -1823,7 +1822,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"lazy_static",
"lazycell",
"log",
@@ -1846,7 +1845,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"log",
"prettyplease",
"proc-macro2",
@@ -2669,7 +2668,7 @@ dependencies = [
"serde",
"tempfile",
"util",
"windows 0.58.0",
"windows 0.60.0",
]
[[package]]
@@ -2711,7 +2710,7 @@ dependencies = [
"tokio-socks",
"url",
"util",
"windows 0.58.0",
"windows 0.60.0",
"worktree",
]
@@ -4457,7 +4456,6 @@ dependencies = [
"env_logger 0.11.6",
"feature_flags",
"fs",
"git",
"gpui",
"http_client",
"language",
@@ -5053,7 +5051,7 @@ dependencies = [
"text",
"time",
"util",
"windows 0.58.0",
"windows 0.60.0",
]
[[package]]
@@ -5450,7 +5448,6 @@ dependencies = [
"db",
"editor",
"env_logger 0.11.6",
"feature_flags",
"futures 0.3.31",
"fuzzy",
"git",
@@ -5478,10 +5475,11 @@ dependencies = [
"telemetry",
"theme",
"time",
"time_format",
"ui",
"unindent",
"util",
"windows 0.58.0",
"windows 0.60.0",
"workspace",
"zed_actions",
]
@@ -5681,8 +5679,8 @@ dependencies = [
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"windows 0.58.0",
"windows-core 0.58.0",
"windows 0.60.0",
"windows-core 0.60.1",
"x11-clipboard",
"x11rb",
"xim",
@@ -7329,7 +7327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -10507,7 +10505,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.1",
"heck 0.5.0",
"itertools 0.12.1",
"itertools 0.10.5",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10540,7 +10538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.12.1",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -11915,19 +11913,20 @@ name = "scripting_tool"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_tool",
"collections",
"futures 0.3.31",
"gpui",
"language",
"log",
"mlua",
"parking_lot",
"project",
"rand 0.8.5",
"regex",
"schemars",
"serde",
"serde_json",
"settings",
"shlex",
"util",
]
@@ -13614,7 +13613,7 @@ dependencies = [
"theme",
"thiserror 1.0.69",
"util",
"windows 0.58.0",
"windows 0.60.0",
]
[[package]]
@@ -13991,7 +13990,7 @@ dependencies = [
"tree-sitter-md",
"ui",
"util",
"windows 0.58.0",
"windows 0.60.0",
"workspace",
"zed_actions",
"zeta",
@@ -14698,7 +14697,7 @@ dependencies = [
"theme",
"ui_macros",
"util",
"windows 0.58.0",
"windows 0.60.0",
]
[[package]]
@@ -15913,7 +15912,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -15970,6 +15969,28 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529"
dependencies = [
"windows-collections",
"windows-core 0.60.1",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec"
dependencies = [
"windows-core 0.60.1",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -16010,10 +16031,33 @@ dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.60.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
dependencies = [
"windows-implement 0.59.0",
"windows-interface 0.59.0",
"windows-link",
"windows-result 0.3.1",
"windows-strings 0.3.1",
]
[[package]]
name = "windows-future"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0"
dependencies = [
"windows-core 0.60.1",
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
@@ -16036,6 +16080,17 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-implement"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -16058,12 +16113,33 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-interface"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
dependencies = [
"proc-macro2",
"quote",
"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-numerics"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed"
dependencies = [
"windows-core 0.60.1",
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -16071,7 +16147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
@@ -16093,6 +16169,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
@@ -16103,6 +16188,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -16985,7 +17079,6 @@ dependencies = [
"repl",
"reqwest_client",
"rope",
"scripting_tool",
"search",
"serde",
"serde_json",
@@ -17020,7 +17113,7 @@ dependencies = [
"vim",
"vim_mode_setting",
"welcome",
"windows 0.58.0",
"windows 0.60.0",
"winresource",
"workspace",
"zed_actions",
@@ -17117,13 +17210,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_terraform"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_test_extension"
version = "0.1.0"

View File

@@ -178,7 +178,6 @@ members = [
"extensions/ruff",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/terraform",
"extensions/test-extension",
"extensions/toml",
"extensions/uiua",
@@ -370,7 +369,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -597,12 +596,12 @@ features = [
]
[workspace.dependencies.windows]
version = "0.58"
version = "0.60"
features = [
"implement",
"Foundation_Collections",
"Foundation_Numerics",
"Storage",
"Storage_Search",
"Storage_Streams",
"System_Threading",
"UI_StartScreen",
"UI_ViewManagement",
@@ -623,9 +622,11 @@ features = [
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_DataExchange",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Pipes",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
@@ -751,5 +752,9 @@ new_ret_no_self = { level = "allow" }
should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 13L1.5 5H11.5L6.5 13Z" fill="black"/>
<path d="M14 9H9L11.5 5L14 9Z" fill="black" fill-opacity="0.75"/>
<path d="M9 9L14 9L11.5 13L9 9Z" fill="black" fill-opacity="0.65"/>
<path d="M14 5L15.25 7L12.75 7L14 5Z" fill="black" fill-opacity="0.5"/>
<path d="M14 9L12.75 7H15.25L14 9Z" fill="black" fill-opacity="0.55"/>
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -731,28 +731,48 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"alt-shift-y": "git::UnstageFile",
"ctrl-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm"
"alt-enter": "menu::SecondaryConfirm",
"backspace": "git::RestoreFile"
}
},
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
},
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit"
"ctrl-enter": "git::Commit",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
},
{
@@ -767,6 +787,7 @@
"escape": "git_panel::FocusChanges",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"
@@ -840,21 +861,22 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
"ctrl-shift-l": "terminal::Clear",
"ctrl-shift-w": "pane::CloseActiveItem",
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"up": ["terminal::SendKeystroke", "up"],
"pageup": ["terminal::SendKeystroke", "pageup"],
"down": ["terminal::SendKeystroke", "down"],
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"shift-pageup": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
"shift-up": "terminal::ScrollLineUp",

View File

@@ -31,13 +31,13 @@
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-escape": "menu::Cancel",
"cmd-o": "workspace::Open",
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -763,28 +763,25 @@
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"cmd-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"cmd-shift-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"cmd-y": "git::StageFile",
"cmd-shift-y": "git::UnstageFile",
"alt-down": "git_panel::FocusEditor",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit"
"cmd-enter": "git::Commit",
"backspace": "git::RestoreFile"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit"
}
},
{
"context": "AskPass > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
"cmd-enter": "git::Commit",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
},
{
@@ -800,11 +797,27 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
"alt-tab": "git::GenerateCommitMessage"
}

View File

@@ -1300,8 +1300,7 @@
},
// Settings for auto-closing of JSX tags.
"jsx_tag_auto_close": {
// // Whether to auto-close JSX tags.
// "enabled": true
"enabled": true
},
// LSP Specific settings.
"lsp": {

View File

@@ -383,6 +383,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -771,6 +776,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -1159,6 +1169,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -1547,6 +1562,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,
@@ -1935,6 +1955,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,
@@ -2323,6 +2348,11 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,

View File

@@ -386,7 +386,6 @@ impl InlineAssistant {
}
}
#[allow(clippy::too_many_arguments)]
pub fn suggest_assist(
&mut self,
editor: &Entity<Editor>,
@@ -1674,7 +1673,6 @@ impl Focusable for PromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -2333,7 +2331,6 @@ struct InlineAssist {
}
impl InlineAssist {
#[allow(clippy::too_many_arguments)]
fn new(
assist_id: InlineAssistId,
group_id: InlineAssistGroupId,

View File

@@ -702,7 +702,6 @@ impl Focusable for PromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,

View File

@@ -59,6 +59,7 @@ prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
scripting_tool.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -81,8 +82,8 @@ zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
indoc.workspace = true

View File

@@ -10,6 +10,7 @@ use gpui::{
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use scripting_tool::{ScriptingTool, ScriptingToolInput};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, KeyBinding};
@@ -28,6 +29,7 @@ pub struct ActiveThread {
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
last_error: Option<ThreadError>,
@@ -58,6 +60,7 @@ impl ActiveThread {
save_thread_task: None,
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
rendered_scripting_tool_uses: HashMap::default(),
expanded_tool_uses: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
@@ -73,6 +76,16 @@ impl ActiveThread {
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), window, cx);
for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
this.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
}
}
this
@@ -239,9 +252,35 @@ impl ActiveThread {
})
}
/// Renders the input of a scripting tool use to Markdown.
///
/// Does nothing if the tool use does not correspond to the scripting tool.
fn render_scripting_tool_use_markdown(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: &str,
tool_input: serde_json::Value,
window: &mut Window,
cx: &mut Context<Self>,
) {
if tool_name != ScriptingTool::NAME {
return;
}
let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
.map(|input| input.lua_script)
.unwrap_or_default();
let lua_script =
self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
self.rendered_scripting_tool_uses
.insert(tool_use_id, lua_script);
}
fn handle_thread_event(
&mut self,
_: &Entity<Thread>,
_thread: &Entity<Thread>,
event: &ThreadEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -296,7 +335,19 @@ impl ActiveThread {
thread.use_pending_tools(cx);
});
}
ThreadEvent::ToolFinished { .. } => {
ThreadEvent::ToolFinished {
pending_tool_use, ..
} => {
if let Some(tool_use) = pending_tool_use {
self.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
}
if self.thread.read(cx).all_tools_finished() {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(model) = model_registry.active_model() {
@@ -445,12 +496,17 @@ impl ActiveThread {
return Empty.into_any();
};
let context = self.thread.read(cx).context_for_message(message_id);
let tool_uses = self.thread.read(cx).tool_uses_for_message(message_id);
let colors = cx.theme().colors();
let thread = self.thread.read(cx);
let context = thread.context_for_message(message_id);
let tool_uses = thread.tool_uses_for_message(message_id);
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User && self.thread.read(cx).message_has_tool_results(message_id) {
if message.role == Role::User
&& (thread.message_has_tool_results(message_id)
|| thread.message_has_scripting_tool_results(message_id))
{
return Empty.into_any();
}
@@ -463,6 +519,8 @@ impl ActiveThread {
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state.editor.clone());
let colors = cx.theme().colors();
let message_content = v_flex()
.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
@@ -598,16 +656,22 @@ impl ActiveThread {
.id(("message-container", ix))
.child(message_content)
.map(|parent| {
if tool_uses.is_empty() {
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
return parent;
}
parent.child(
v_flex().children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
),
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
)
.children(
scripting_tool_uses
.into_iter()
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
),
)
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
@@ -641,8 +705,13 @@ impl ActiveThread {
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
.when(!is_open, |element| element.rounded_md())
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
} else {
element.rounded_md()
}
})
.border_color(cx.theme().colors().border)
.child(
h_flex()
@@ -716,6 +785,119 @@ impl ActiveThread {
}),
)
}
fn render_scripting_tool_use(
&self,
tool_use: ToolUse,
cx: &mut Context<Self>,
) -> impl IntoElement {
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
.copied()
.unwrap_or_default();
div().px_2p5().child(
v_flex()
.gap_1()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_between()
.py_0p5()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
} else {
element.rounded_md()
}
})
.border_color(cx.theme().colors().border)
.child(
h_flex()
.gap_1()
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
cx.listener({
let tool_use_id = tool_use.id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_tool_uses
.entry(tool_use_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
}),
))
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
)
.map(|parent| {
if !is_open {
return parent;
}
let lua_script_markdown =
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
parent.child(
v_flex()
.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Input:"))
.map(|parent| {
if let Some(markdown) = lua_script_markdown {
parent.child(markdown)
} else {
parent.child(Label::new(
"Failed to render script input to Markdown",
))
}
}),
)
.map(|parent| match tool_use.status {
ToolUseStatus::Finished(output) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Result:"))
.child(Label::new(output)),
),
ToolUseStatus::Error(err) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Error:"))
.child(Label::new(err)),
),
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
}),
)
}),
)
}
}
impl Render for ActiveThread {

View File

@@ -16,6 +16,7 @@ mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_selector;
mod tool_use;
mod ui;

View File

@@ -166,22 +166,24 @@ impl AssistantPanel {
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
let thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
language_registry.clone(),
window,
cx,
)
});
Self {
active_view: ActiveView::Thread,
workspace,
project: project.clone(),
fs: fs.clone(),
language_registry: language_registry.clone(),
language_registry,
thread_store: thread_store.clone(),
thread: cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
language_registry,
window,
cx,
)
}),
thread,
message_editor,
context_store,
context_editor: None,

View File

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

View File

@@ -25,7 +25,7 @@ use crate::{
pub struct ContextStrip {
context_store: Entity<ContextStore>,
pub context_picker: Entity<ContextPicker>,
context_picker: Entity<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
@@ -36,7 +36,6 @@ pub struct ContextStrip {
}
impl ContextStrip {
#[allow(clippy::too_many_arguments)]
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,

View File

@@ -480,7 +480,6 @@ impl InlineAssistant {
}
}
#[allow(clippy::too_many_arguments)]
pub fn suggest_assist(
&mut self,
editor: &Entity<Editor>,
@@ -1451,7 +1450,6 @@ struct InlineAssistScrollLock {
}
impl EditorInlineAssists {
#[allow(clippy::too_many_arguments)]
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
Self {
@@ -1563,7 +1561,6 @@ pub struct InlineAssist {
}
impl InlineAssist {
#[allow(clippy::too_many_arguments)]
fn new(
assist_id: InlineAssistId,
group_id: InlineAssistGroupId,

View File

@@ -816,7 +816,6 @@ impl InlineAssistId {
}
impl PromptEditor<BufferCodegen> {
#[allow(clippy::too_many_arguments)]
pub fn new_buffer(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -976,7 +975,6 @@ impl TerminalInlineAssistId {
}
impl PromptEditor<TerminalCodegen> {
#[allow(clippy::too_many_arguments)]
pub fn new_terminal(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
@@ -15,8 +16,8 @@ use std::time::Duration;
use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
Tooltip,
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
Switch, Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
@@ -27,6 +28,7 @@ use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::tool_selector::ToolSelector;
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
pub struct MessageEditor {
@@ -38,7 +40,9 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
tool_selector: Entity<ToolSelector>,
use_tools: bool,
edits_expanded: bool,
_subscriptions: Vec<Subscription>,
}
@@ -51,6 +55,7 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let tools = thread.read(cx).tools().clone();
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
@@ -116,7 +121,9 @@ impl MessageEditor {
cx,
)
}),
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
use_tools: false,
edits_expanded: false,
_subscriptions: subscriptions,
}
}
@@ -303,6 +310,9 @@ impl Render for MessageEditor {
px(64.)
};
let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx);
let changed_buffers_count = changed_buffers.len();
v_flex()
.size_full()
.when(is_streaming_completion, |parent| {
@@ -363,6 +373,109 @@ impl Render for MessageEditor {
),
)
})
.when(changed_buffers_count > 0, |parent| {
parent.child(
v_flex()
.mx_2()
.bg(cx.theme().colors().element_background)
.border_1()
.border_b_0()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.child(
h_flex()
.gap_2()
.p_2()
.child(
Disclosure::new("edits-disclosure", self.edits_expanded)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
})),
)
.child(
Label::new("Edits")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
.child(
Label::new(format!(
"{} {}",
changed_buffers_count,
if changed_buffers_count == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.when(self.edits_expanded, |parent| {
parent.child(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.enumerate().flat_map(|(index, buffer)| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::Small),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::Small)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
let element = div()
.p_2()
.when(index + 1 < changed_buffers_count, |parent| {
parent
.border_color(cx.theme().colors().border)
.border_b_1()
})
.child(
h_flex()
.gap_2()
.child(file_icon)
.child(
// TODO: handle overflow
h_flex()
.children(parent_label)
.children(name_label),
)
// TODO: show lines changed
.child(Label::new("+").color(Color::Created))
.child(Label::new("-").color(Color::Deleted)),
);
Some(element)
}),
),
)
}),
)
})
.child(
v_flex()
.key_context("MessageEditor")
@@ -429,23 +542,25 @@ impl Render for MessageEditor {
h_flex()
.justify_between()
.child(
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(
|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
},
))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
h_flex().gap_2().child(self.tool_selector.clone()).child(
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(
|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
},
))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
),
)
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(

View File

@@ -5,7 +5,7 @@ use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::StreamExt as _;
use gpui::{App, Context, Entity, EventEmitter, SharedString, Task};
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
@@ -13,6 +13,7 @@ use language_model::{
Role, StopReason,
};
use project::Project;
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
@@ -75,14 +76,18 @@ pub struct Thread {
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
tool_use: ToolUseState,
scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState,
}
impl Thread {
pub fn new(
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) -> Self {
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -97,6 +102,8 @@ impl Thread {
project,
tools,
tool_use: ToolUseState::new(),
scripting_session,
scripting_tool_use: ToolUseState::new(),
}
}
@@ -105,7 +112,7 @@ impl Thread {
saved: SavedThread,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
saved
@@ -114,7 +121,11 @@ impl Thread {
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
let tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
let scripting_tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
Self {
id,
@@ -138,6 +149,8 @@ impl Thread {
project,
tools,
tool_use,
scripting_session,
scripting_tool_use,
}
}
@@ -198,42 +211,65 @@ impl Thread {
)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.tool_use.pending_tool_uses()
}
/// Returns whether all of the tool uses have finished running.
pub fn all_tools_finished(&self) -> bool {
let mut all_pending_tool_uses = self
.tool_use
.pending_tool_uses()
.into_iter()
.chain(self.scripting_tool_use.pending_tool_uses());
// If the only pending tool uses left are the ones with errors, then that means that we've finished running all
// of the pending tools.
self.pending_tool_uses()
.into_iter()
.all(|tool_use| tool_use.status.is_error())
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
}
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id)
}
pub fn scripting_tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.scripting_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 scripting_tool_results_for_message(
&self,
id: MessageId,
) -> Vec<&LanguageModelToolResult> {
self.scripting_tool_use.tool_results_for_message(id)
}
pub fn scripting_changed_buffers<'a>(
&self,
cx: &'a App,
) -> impl ExactSizeIterator<Item = &'a Entity<language::Buffer>> {
self.scripting_session.read(cx).changed_buffers()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
self.scripting_tool_use.message_has_tool_results(message_id)
}
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<ContextSnapshot>,
cx: &mut Context<Self>,
) {
) -> MessageId {
let message_id = self.insert_message(Role::User, text, cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
self.context
.extend(context.into_iter().map(|context| (context.id, context)));
self.context_by_message.insert(message_id, context_ids);
message_id
}
pub fn insert_message(
@@ -312,16 +348,22 @@ impl Thread {
let mut request = self.to_completion_request(request_kind, cx);
if use_tools {
request.tools = self
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
let mut tools = Vec::new();
tools.push(LanguageModelRequestTool {
name: ScriptingTool::NAME.into(),
description: ScriptingTool::DESCRIPTION.into(),
input_schema: ScriptingTool::input_schema(),
});
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
}));
request.tools = tools;
}
self.stream_completion(request, model, cx);
@@ -351,10 +393,13 @@ impl Thread {
content: Vec::new(),
cache: false,
};
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
self.scripting_tool_use
.attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
@@ -371,11 +416,13 @@ impl Thread {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
self.scripting_tool_use
.attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
}
}
};
request.messages.push(request_message);
}
@@ -439,7 +486,7 @@ impl Thread {
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(Role::Assistant, chunk, cx);
}
};
}
}
LanguageModelCompletionEvent::ToolUse(tool_use) => {
@@ -448,9 +495,15 @@ impl Thread {
.iter()
.rfind(|message| message.role == Role::Assistant)
{
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
if tool_use.name.as_ref() == ScriptingTool::NAME {
thread
.scripting_tool_use
.request_tool_use(last_assistant_message.id, tool_use);
} else {
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
}
}
}
}
@@ -570,6 +623,7 @@ impl Thread {
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
@@ -583,6 +637,45 @@ impl Thread {
self.insert_tool_output(tool_use.id.clone(), task, cx);
}
}
let pending_scripting_tool_uses = self
.scripting_tool_use
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
for scripting_tool_use in pending_scripting_tool_uses {
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input) {
Err(err) => Task::ready(Err(err.into())),
Ok(input) => {
let (script_id, script_task) =
self.scripting_session.update(cx, move |session, cx| {
session.run_script(input.lua_script, cx)
});
let session = self.scripting_session.clone();
cx.spawn(|_, cx| async move {
script_task.await;
let message = session.read_with(&cx, |session, _cx| {
// Using a id to get the script output seems impractical.
// Why not just include it in the Task result?
// This is because we'll later report the script state as it runs,
session
.get(script_id)
.output_message_for_llm()
.expect("Script shouldn't still be running")
})?;
Ok(message)
})
}
};
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), task, cx);
}
}
pub fn insert_tool_output(
@@ -597,11 +690,14 @@ impl Thread {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
thread
let pending_tool_use = thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
});
})
.ok();
}
@@ -611,6 +707,35 @@ impl Thread {
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn insert_scripting_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
async move {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
let pending_tool_use = thread
.scripting_tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
});
})
.ok();
}
});
self.scripting_tool_use
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn send_tool_results_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
@@ -660,6 +785,8 @@ pub enum ThreadEvent {
ToolFinished {
#[allow(unused)]
tool_use_id: LanguageModelToolUseId,
/// The pending tool use that corresponds to this tool.
pending_tool_use: Option<PendingToolUse>,
},
}

View File

@@ -116,28 +116,35 @@ impl ThreadStore {
updated_at: thread.updated_at(),
messages: thread
.messages()
.map(|message| SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: thread
.map(|message| {
let all_tool_uses = thread
.tool_uses_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_uses_for_message(message.id))
.map(|tool_use| SavedToolUse {
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
})
.collect(),
tool_results: thread
.collect();
let all_tool_results = thread
.tool_results_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_results_for_message(message.id))
.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();
SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: all_tool_uses,
tool_results: all_tool_results,
}
})
.collect(),
};

View File

@@ -0,0 +1,70 @@
use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::Entity;
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
pub struct ToolSelector {
tools: Arc<ToolWorkingSet>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self { tools }
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let tools_by_source = self.tools.tools_by_source(cx);
for (source, tools) in tools_by_source {
menu = match source {
ToolSource::Native => menu.header("Zed"),
ToolSource::ContextServer { id } => menu.separator().header(id),
};
for tool in tools {
let source = tool.source();
let name = tool.name().into();
let is_enabled = self.tools.is_enabled(&source, &name);
menu =
menu.toggleable_entry(tool.name(), is_enabled, IconPosition::End, None, {
let tools = self.tools.clone();
move |_window, _cx| {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
}
});
}
}
menu
})
}
}
impl Render for ToolSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
IconButton::new("tool-selector-button", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
Tooltip::text("Customize Tools"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

View File

@@ -46,25 +46,39 @@ impl ToolUseState {
}
}
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_saved_messages(
messages: &[SavedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
let mut this = Self::new();
let mut tool_names_by_id = HashMap::default();
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
let tool_uses = message
.tool_uses
.iter()
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
})
.collect::<Vec<_>>();
tool_names_by_id.extend(
tool_uses
.iter()
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
})
.collect(),
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
);
this.tool_uses_by_assistant_message
.insert(message.id, tool_uses);
}
}
Role::User => {
@@ -76,6 +90,14 @@ impl ToolUseState {
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
log::warn!("no tool name found for tool use: {tool_use_id:?}");
continue;
};
if !(filter_by_tool_name)(tool_use.as_ref()) {
continue;
}
tool_uses_by_user_message.push(tool_use_id.clone());
this.tool_results.insert(
@@ -202,7 +224,7 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Result<String>,
) {
) -> Option<PendingToolUse> {
match output {
Ok(output) => {
self.tool_results.insert(
@@ -213,7 +235,7 @@ impl ToolUseState {
is_error: false,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id);
self.pending_tool_uses_by_id.remove(&tool_use_id)
}
Err(err) => {
self.tool_results.insert(
@@ -228,6 +250,8 @@ impl ToolUseState {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
}
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
}
}
}
@@ -267,6 +291,7 @@ impl ToolUseState {
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub input: serde_json::Value,

View File

@@ -647,7 +647,6 @@ impl AssistantContext {
)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
id: ContextId,
replica_id: ReplicaId,
@@ -768,7 +767,6 @@ impl AssistantContext {
}
}
#[allow(clippy::too_many_arguments)]
pub fn deserialize(
saved_context: SavedContext,
path: PathBuf,

View File

@@ -535,7 +535,6 @@ impl ContextEditor {
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_command(
&mut self,
command_range: Range<language::Anchor>,
@@ -2057,7 +2056,6 @@ impl ContextEditor {
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
}
#[allow(clippy::too_many_arguments)]
fn render_patch_block(
&mut self,
range: Range<text::Anchor>,

View File

@@ -134,7 +134,6 @@ impl SlashCommandCompletionProvider {
})
}
#[allow(clippy::too_many_arguments)]
fn complete_command_argument(
&self,
command_name: &str,

View File

@@ -88,7 +88,6 @@ pub trait SlashCommand: 'static + Send + Sync {
fn accepts_arguments(&self) -> bool {
self.requires_argument()
}
#[allow(clippy::too_many_arguments)]
fn run(
self: Arc<Self>,
arguments: &[String],

View File

@@ -4,7 +4,7 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::{App, Entity, Task};
use gpui::{App, Entity, SharedString, Task};
use project::Project;
pub use crate::tool_registry::*;
@@ -14,6 +14,14 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum ToolSource {
/// A native tool built-in to Zed.
Native,
/// A tool provided by a context server.
ContextServer { id: SharedString },
}
/// A tool that can be used by a language model.
pub trait Tool: 'static + Send + Sync {
/// Returns the name of the tool.
@@ -22,6 +30,11 @@ pub trait Tool: 'static + Send + Sync {
/// Returns the description of the tool.
fn description(&self) -> String;
/// Returns the source of the tool.
fn source(&self) -> ToolSource {
ToolSource::Native
}
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self) -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::default())

View File

@@ -1,10 +1,10 @@
use std::sync::Arc;
use collections::HashMap;
use collections::{HashMap, HashSet, IndexMap};
use gpui::App;
use parking_lot::Mutex;
use crate::{Tool, ToolRegistry};
use crate::{Tool, ToolRegistry, ToolSource};
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct ToolId(usize);
@@ -19,6 +19,7 @@ pub struct ToolWorkingSet {
struct WorkingSetState {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
next_tool_id: ToolId,
}
@@ -45,22 +46,80 @@ impl ToolWorkingSet {
tools
}
pub fn insert(&self, command: Arc<dyn Tool>) -> ToolId {
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let all_tools = self.tools(cx);
all_tools
.into_iter()
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
.collect()
}
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
let mut tools_by_source = IndexMap::default();
for tool in self.tools(cx) {
tools_by_source
.entry(tool.source())
.or_insert_with(Vec::new)
.push(tool);
}
for tools in tools_by_source.values_mut() {
tools.sort_by_key(|tool| tool.name());
}
tools_by_source.sort_unstable_keys();
tools_by_source
}
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
let mut state = self.state.lock();
let command_id = state.next_tool_id;
let tool_id = state.next_tool_id;
state.next_tool_id.0 += 1;
state
.context_server_tools_by_id
.insert(command_id, command.clone());
.insert(tool_id, tool.clone());
state.tools_changed();
command_id
tool_id
}
pub fn remove(&self, command_ids_to_remove: &[ToolId]) {
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
!self.is_disabled(source, name)
}
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
let state = self.state.lock();
state
.disabled_tools_by_source
.get(source)
.map_or(false, |disabled_tools| disabled_tools.contains(name))
}
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
let mut state = self.state.lock();
state
.disabled_tools_by_source
.entry(source)
.or_default()
.retain(|name| !tools_to_enable.contains(name));
}
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
let mut state = self.state.lock();
state
.disabled_tools_by_source
.entry(source)
.or_default()
.extend(tools_to_disable.into_iter().cloned());
}
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
let mut state = self.state.lock();
state
.context_server_tools_by_id
.retain(|id, _| !command_ids_to_remove.contains(id));
.retain(|id, _| !tool_ids_to_remove.contains(id));
state.tools_changed();
}
}
@@ -71,7 +130,7 @@ impl WorkingSetState {
self.context_server_tools_by_name.extend(
self.context_server_tools_by_id
.values()
.map(|command| (command.name(), command.clone())),
.map(|tool| (tool.name(), tool.clone())),
);
}
}

View File

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

View File

@@ -229,7 +229,6 @@ impl Database {
}
/// Creates a new channel message.
#[allow(clippy::too_many_arguments)]
pub async fn create_channel_message(
&self,
channel_id: ChannelId,

View File

@@ -122,7 +122,6 @@ impl Database {
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn get_or_create_user_by_github_account_tx(
&self,
github_login: &str,

View File

@@ -289,7 +289,6 @@ impl LlmDatabase {
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn record_usage(
&self,
user_id: UserId,
@@ -554,7 +553,6 @@ impl LlmDatabase {
.await
}
#[allow(clippy::too_many_arguments)]
async fn update_usage_for_measure(
&self,
user_id: UserId,

View File

@@ -33,7 +33,6 @@ pub struct LlmTokenClaims {
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
impl LlmTokenClaims {
#[allow(clippy::too_many_arguments)]
pub fn create(
user: &user::Model,
is_staff: bool,

View File

@@ -697,7 +697,6 @@ impl Server {
})
}
#[allow(clippy::too_many_arguments)]
pub fn handle_connection(
self: &Arc<Self>,
connection: Connection,
@@ -1081,7 +1080,6 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
.layer(Extension(server))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
app_version_header: Option<TypedHeader<AppVersionHeader>>,

View File

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

View File

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

View File

@@ -463,7 +463,6 @@ impl<T: RandomizedTest> TestPlan<T> {
})
}
#[allow(clippy::too_many_arguments)]
async fn apply_server_operation(
plan: Arc<Mutex<Self>>,
deterministic: BackgroundExecutor,

View File

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

View File

@@ -869,7 +869,6 @@ impl CollabPanel {
})
}
#[allow(clippy::too_many_arguments)]
fn render_participant_project(
&self,
project_id: u64,

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use assistant_tool::Tool;
use assistant_tool::{Tool, ToolSource};
use gpui::{App, Entity, Task};
use project::Project;
@@ -37,6 +37,12 @@ impl Tool for ContextServerTool {
self.tool.description.clone().unwrap_or_default()
}
fn source(&self) -> ToolSource {
ToolSource::ContextServer {
id: self.server_id.clone().into(),
}
}
fn input_schema(&self) -> serde_json::Value {
match &self.tool.input_schema {
serde_json::Value::Null => {

View File

@@ -623,16 +623,21 @@ impl Copilot {
pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
let server = server.clone();
cx.background_spawn(async move {
server
.request::<request::SignOut>(request::SignOutParams {})
.await?;
match &self.server {
CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
let server = server.clone();
cx.background_spawn(async move {
server
.request::<request::SignOut>(request::SignOutParams {})
.await?;
anyhow::Ok(())
})
}
CopilotServer::Disabled => cx.background_spawn(async move {
clear_copilot_config_dir().await;
anyhow::Ok(())
})
} else {
Task::ready(Err(anyhow!("copilot hasn't started yet")))
}),
_ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
}
}
@@ -1016,6 +1021,10 @@ async fn clear_copilot_dir() {
remove_matching(paths::copilot_dir(), |_| true).await
}
async fn clear_copilot_config_dir() {
remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
}
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
const SERVER_PATH: &str = "dist/language-server.js";

View File

@@ -4,13 +4,14 @@ use std::sync::OnceLock;
use anyhow::{anyhow, Result};
use chrono::DateTime;
use collections::HashSet;
use fs::Fs;
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use gpui::{prelude::*, App, AsyncApp, Global};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_file;
use settings::watch_config_dir;
use strum::EnumIter;
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
@@ -212,7 +213,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut App) {
cx.set_global(GlobalCopilotChat(copilot_chat));
}
fn copilot_chat_config_dir() -> &'static PathBuf {
pub fn copilot_chat_config_dir() -> &'static PathBuf {
static COPILOT_CHAT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
COPILOT_CHAT_CONFIG_DIR.get_or_init(|| {
@@ -237,27 +238,18 @@ impl CopilotChat {
}
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
let config_paths = copilot_chat_config_paths();
let resolve_config_path = {
let fs = fs.clone();
async move {
for config_path in config_paths.iter() {
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
return config_path.clone();
}
}
config_paths[0].clone()
}
};
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
cx.spawn(|cx| async move {
let config_file = resolve_config_path.await;
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
while let Some(contents) = config_file_rx.next().await {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {

View File

@@ -311,7 +311,10 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Workspace>,
) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
workspace.activate_item(&existing, true, true, window, cx);
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let workspace_handle = cx.entity().downgrade();

View File

@@ -500,7 +500,7 @@ impl CompletionsMenu {
highlight.font_weight = None;
if completion
.source
.lsp_completion()
.lsp_completion(false)
.and_then(|lsp_completion| lsp_completion.deprecated)
.unwrap_or(false)
{
@@ -711,10 +711,12 @@ impl CompletionsMenu {
let completion = &completions[mat.candidate_id];
let sort_key = completion.sort_key();
let sort_text = completion
.source
.lsp_completion()
.and_then(|lsp_completion| lsp_completion.sort_text.as_deref());
let sort_text =
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
lsp_completion.sort_text.as_deref()
} else {
None
};
let score = Reverse(OrderedFloat(mat.score));
if mat.score >= 0.2 {

View File

@@ -113,7 +113,6 @@ pub struct DisplayMap {
}
impl DisplayMap {
#[allow(clippy::too_many_arguments)]
pub fn new(
buffer: Entity<MultiBuffer>,
font: Font,

View File

@@ -726,7 +726,6 @@ impl BlockMap {
self.show_excerpt_controls
}
#[allow(clippy::too_many_arguments)]
fn header_and_footer_blocks<'a, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u32,

View File

@@ -5931,7 +5931,6 @@ impl Editor {
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6043,7 +6042,6 @@ impl Editor {
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_modifier_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6139,7 +6137,6 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_scroll_popover(
&mut self,
to_y: impl Fn(Size<Pixels>) -> Pixels,
@@ -6170,7 +6167,6 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_eager_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6240,7 +6236,6 @@ impl Editor {
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_end_of_line_popover(
self: &mut Editor,
label: &'static str,
@@ -6299,7 +6294,6 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_diff_popover(
self: &Editor,
text_bounds: &Bounds<Pixels>,
@@ -6607,7 +6601,6 @@ impl Editor {
editor_bg_color.blend(accent_color.opacity(0.6))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_cursor_popover(
&self,
min_width: Pixels,
@@ -11639,7 +11632,7 @@ impl Editor {
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_or_before_position(
self.go_to_hunk_before_or_after_position(
&snapshot,
selection.head(),
Direction::Next,
@@ -11648,7 +11641,7 @@ impl Editor {
);
}
fn go_to_hunk_after_or_before_position(
fn go_to_hunk_before_or_after_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
@@ -11699,7 +11692,7 @@ impl Editor {
) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_after_or_before_position(
self.go_to_hunk_before_or_after_position(
&snapshot,
selection.head(),
Direction::Prev,
@@ -13861,21 +13854,6 @@ impl Editor {
return;
}
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let run_twice = snapshot
.hunks_for_ranges([newest_range])
.first()
.is_some_and(|hunk| {
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
self.hunk_after_position(&snapshot, next_line)
.is_some_and(|other| other.row_range == hunk.row_range)
});
if run_twice {
self.go_to_next_hunk(&GoToHunk, window, cx);
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
self.go_to_next_hunk(&GoToHunk, window, cx);
}
@@ -14303,6 +14281,13 @@ impl Editor {
EditorSettings::override_global(editor_settings, cx);
}
pub fn line_numbers_enabled(&self, cx: &App) -> bool {
if let Some(show_line_numbers) = self.show_line_numbers {
return show_line_numbers;
}
EditorSettings::get_global(cx).gutter.line_numbers
}
pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool {
self.use_relative_line_numbers
.unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
@@ -17017,6 +17002,7 @@ fn snippet_completions(
sort_text: Some(char::MAX.to_string()),
..lsp::CompletionItem::default()
}),
lsp_defaults: None,
},
label: CodeLabel {
text: matching_prefix.clone(),

View File

@@ -12334,24 +12334,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -12370,10 +12352,11 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
let completion_items = items.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
let items = completion_items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items,
@@ -12422,7 +12405,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
.iter()
.map(|mat| mat.string.clone())
.collect::<Vec<String>>(),
items_out
items
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
@@ -12435,14 +12418,18 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
// with 4 from the end.
assert_eq!(
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
[&items[0..16], &items[items.len() - 4..items.len()]]
.concat()
.iter()
.cloned()
.map(|mut item| {
if item.data.is_none() {
item.data = Some(default_data.clone());
}
item
})
.collect::<Vec<lsp::CompletionItem>>(),
"Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
);
resolved_items.lock().clear();
@@ -12453,9 +12440,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
// Completions that have already been resolved are skipped.
assert_eq!(
*resolved_items.lock(),
items_out[items_out.len() - 16..items_out.len() - 4]
items[items.len() - 16..items.len() - 4]
.iter()
.cloned()
.map(|mut item| {
if item.data.is_none() {
item.data = Some(default_data.clone());
}
item
})
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();

View File

@@ -958,7 +958,6 @@ impl EditorElement {
cx.notify()
}
#[allow(clippy::too_many_arguments)]
fn layout_selections(
&self,
start_anchor: Anchor,
@@ -1130,7 +1129,6 @@ impl EditorElement {
cursors
}
#[allow(clippy::too_many_arguments)]
fn layout_visible_cursors(
&self,
snapshot: &EditorSnapshot,
@@ -1484,7 +1482,6 @@ impl EditorElement {
axis_pair(horizontal_scrollbar, vertical_scrollbar)
}
#[allow(clippy::too_many_arguments)]
fn prepaint_crease_toggles(
&self,
crease_toggles: &mut [Option<AnyElement>],
@@ -1519,7 +1516,6 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_crease_trailers(
&self,
trailers: Vec<Option<AnyElement>>,
@@ -1596,7 +1592,6 @@ impl EditorElement {
display_hunks
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_diagnostics(
&self,
line_layouts: &[LineWithInvisibles],
@@ -1747,7 +1742,6 @@ impl EditorElement {
elements
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_blame(
&self,
display_row: DisplayRow,
@@ -1827,7 +1821,6 @@ impl EditorElement {
Some(element)
}
#[allow(clippy::too_many_arguments)]
fn layout_blame_entries(
&self,
buffer_rows: &[RowInfo],
@@ -1896,7 +1889,6 @@ impl EditorElement {
Some(shaped_lines)
}
#[allow(clippy::too_many_arguments)]
fn layout_indent_guides(
&self,
content_origin: gpui::Point<Pixels>,
@@ -2014,7 +2006,6 @@ impl EditorElement {
(offset_y, length)
}
#[allow(clippy::too_many_arguments)]
fn layout_run_indicators(
&self,
line_height: Pixels,
@@ -2108,7 +2099,6 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -2207,7 +2197,6 @@ impl EditorElement {
relative_rows
}
#[allow(clippy::too_many_arguments)]
fn layout_line_numbers(
&self,
gutter_hitbox: Option<&Hitbox>,
@@ -2423,7 +2412,6 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_lines(
&self,
start_row: DisplayRow,
@@ -2450,7 +2438,6 @@ impl EditorElement {
line_elements
}
#[allow(clippy::too_many_arguments)]
fn render_block(
&self,
block: &Block,
@@ -2950,7 +2937,6 @@ impl EditorElement {
}))
}
#[allow(clippy::too_many_arguments)]
fn render_blocks(
&self,
rows: Range<DisplayRow>,
@@ -3135,7 +3121,6 @@ impl EditorElement {
/// Returns true if any of the blocks changed size since the previous frame. This will trigger
/// a restart of rendering for the editor based on the new sizes.
#[allow(clippy::too_many_arguments)]
fn layout_blocks(
&self,
blocks: &mut Vec<BlockLayout>,
@@ -3179,7 +3164,6 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_sticky_buffer_header(
&self,
StickyHeaderExcerpt {
@@ -3254,7 +3238,6 @@ impl EditorElement {
header
}
#[allow(clippy::too_many_arguments)]
fn layout_cursor_popovers(
&self,
line_height: Pixels,
@@ -3443,7 +3426,6 @@ impl EditorElement {
);
}
#[allow(clippy::too_many_arguments)]
fn layout_gutter_menu(
&self,
line_height: Pixels,
@@ -3496,7 +3478,6 @@ impl EditorElement {
);
}
#[allow(clippy::too_many_arguments)]
fn layout_popovers_above_or_below_line(
&self,
target_position: gpui::Point<Pixels>,
@@ -3610,7 +3591,6 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_context_menu_aside(
&self,
y_flipped: bool,
@@ -3806,7 +3786,6 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_hover_popovers(
&self,
snapshot: &EditorSnapshot,
@@ -3923,7 +3902,6 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_diff_hunk_controls(
&self,
row_range: Range<DisplayRow>,
@@ -4008,7 +3986,6 @@ impl EditorElement {
controls
}
#[allow(clippy::too_many_arguments)]
fn layout_signature_help(
&self,
hitbox: &Hitbox,
@@ -4676,6 +4653,7 @@ impl EditorElement {
};
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
self.paint_redactions(layout, window);
@@ -4766,6 +4744,18 @@ impl EditorElement {
}
}
fn paint_lines_background(
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) {
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
}
}
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
if layout.redacted_ranges.is_empty() {
return;
@@ -5304,7 +5294,6 @@ impl EditorElement {
});
}
#[allow(clippy::too_many_arguments)]
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
@@ -5730,7 +5719,6 @@ impl AcceptEditPredictionBinding {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_button(
button: IconButton,
row: DisplayRow,
@@ -5981,7 +5969,6 @@ impl fmt::Debug for LineFragment {
}
impl LineWithInvisibles {
#[allow(clippy::too_many_arguments)]
fn from_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
editor_style: &EditorStyle,
@@ -6186,7 +6173,6 @@ impl LineWithInvisibles {
layouts
}
#[allow(clippy::too_many_arguments)]
fn prepaint(
&mut self,
line_height: Pixels,
@@ -6221,7 +6207,6 @@ impl LineWithInvisibles {
}
}
#[allow(clippy::too_many_arguments)]
fn draw(
&self,
layout: &EditorLayout,
@@ -6265,7 +6250,35 @@ impl LineWithInvisibles {
);
}
#[allow(clippy::too_many_arguments)]
fn draw_background(
&self,
layout: &EditorLayout,
row: DisplayRow,
content_origin: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) {
let line_height = layout.position_map.line_height;
let line_y = line_height
* (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
let mut fragment_origin =
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
for fragment in &self.fragments {
match fragment {
LineFragment::Text(line) => {
line.paint_background(fragment_origin, line_height, window, cx)
.log_err();
fragment_origin.x += line.width;
}
LineFragment::Element { size, .. } => {
fragment_origin.x += size.width;
}
}
}
}
fn draw_invisibles(
&self,
selection_ranges: &[Range<DisplayPoint>],
@@ -7659,7 +7672,6 @@ struct ScrollbarRangeData {
}
impl ScrollbarRangeData {
#[allow(clippy::too_many_arguments)]
pub fn new(
scrollbar_bounds: Bounds<Pixels>,
letter_size: Size<Pixels>,
@@ -9014,7 +9026,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
@@ -9050,7 +9062,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_after_or_before_position(
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
Direction::Prev,

View File

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

View File

@@ -223,7 +223,6 @@ impl ScrollManager {
self.anchor.scroll_position(snapshot)
}
#[allow(clippy::too_many_arguments)]
fn set_scroll_position(
&mut self,
scroll_position: gpui::Point<f32>,
@@ -298,7 +297,6 @@ impl ScrollManager {
);
}
#[allow(clippy::too_many_arguments)]
fn set_anchor(
&mut self,
anchor: ScrollAnchor,

View File

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

View File

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

View File

@@ -195,7 +195,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
mod wit {
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
wit_bindgen::generate!({
skip: ["init-extension"],

View File

@@ -218,7 +218,6 @@ impl ExtensionStore {
cx.global::<GlobalExtensionStore>().0.clone()
}
#[allow(clippy::too_many_arguments)]
pub fn new(
extensions_dir: PathBuf,
build_dir: Option<PathBuf>,

View File

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

View File

@@ -653,7 +653,6 @@ impl FileSearchQuery {
}
impl FileFinderDelegate {
#[allow(clippy::too_many_arguments)]
fn new(
file_finder: WeakEntity<FileFinder>,
workspace: WeakEntity<Workspace>,

View File

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

View File

@@ -347,12 +347,14 @@ impl PickerDelegate for OpenPathDelegate {
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
{
error
} else {
"No such file or directory".into()
}
};
Some(text)
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {

View File

@@ -11,7 +11,6 @@ use collections::HashMap;
use git::status::StatusCode;
#[cfg(any(test, feature = "test-support"))]
use git::status::TrackedStatus;
use git::GitHostingProviderRegistry;
#[cfg(any(test, feature = "test-support"))]
use git::{repository::RepoPath, status::FileStatus};
@@ -247,7 +246,6 @@ impl From<MTime> for proto::Timestamp {
#[derive(Default)]
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
}
@@ -300,14 +298,8 @@ impl FileHandle for std::fs::File {
pub struct RealWatcher {}
impl RealFs {
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
}
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
Self { git_binary_path }
}
}
@@ -770,7 +762,6 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
}

View File

@@ -170,7 +170,6 @@ impl<'a> Matcher<'a> {
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],

View File

@@ -1,17 +1,15 @@
use crate::commit::get_messages;
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
use crate::Oid;
use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::Stdio;
use std::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
use time::macros::format_description;
use time::OffsetDateTime;
use time::UtcOffset;
use url::Url;
pub use git2 as libgit;
@@ -19,7 +17,6 @@ pub use git2 as libgit;
pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub permalinks: HashMap<Oid, Url>,
pub remote_url: Option<String>,
}
@@ -30,32 +27,15 @@ impl Blame {
path: &Path,
content: &Rope,
remote_url: Option<String>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, content)?;
let mut entries = parse_git_blame(&output)?;
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
let mut permalinks = HashMap::default();
let mut unique_shas = HashSet::default();
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing.
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
permalinks.entry(entry.sha).or_insert_with(|| {
provider.build_commit_permalink(
remote,
BuildCommitPermalinkParams {
sha: entry.sha.to_string().as_str(),
},
)
});
}
}
let shas = unique_shas.into_iter().collect::<Vec<_>>();
@@ -64,7 +44,6 @@ impl Blame {
Ok(Self {
entries,
permalinks,
messages,
remote_url,
})

View File

@@ -1,5 +1,5 @@
use crate::status::FileStatus;
use crate::GitHostingProviderRegistry;
use crate::SHORT_SHA_LENGTH;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{anyhow, Context, Result};
use askpass::{AskPassResult, AskPassSession};
@@ -57,6 +57,14 @@ pub struct Upstream {
pub tracking: UpstreamTracking,
}
impl Upstream {
pub fn remote_name(&self) -> Option<&str> {
self.ref_name
.strip_prefix("refs/remotes/")
.and_then(|stripped| stripped.split("/").next())
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UpstreamTracking {
/// Remote ref not present in local repository.
@@ -120,6 +128,12 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
impl CommitDetails {
pub fn short_sha(&self) -> SharedString {
self.sha[..SHORT_SHA_LENGTH].to_string().into()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Remote {
pub name: SharedString,
@@ -149,7 +163,12 @@ pub trait GitRepository: Send + Sync {
/// Also returns `None` for symlinks.
fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
env: &HashMap<String, String>,
) -> anyhow::Result<()>;
/// Returns the URL of the remote with the given name.
fn remote_url(&self, name: &str) -> Option<String>;
@@ -167,8 +186,13 @@ pub trait GitRepository: Send + Sync {
fn create_branch(&self, _: &str) -> Result<()>;
fn branch_exits(&self, _: &str) -> Result<bool>;
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()>;
fn checkout_files(
&self,
commit: &str,
paths: &[RepoPath],
env: &HashMap<String, String>,
) -> Result<()>;
fn show(&self, commit: &str) -> Result<CommitDetails>;
@@ -189,13 +213,18 @@ pub trait GitRepository: Send + Sync {
/// Updates the index to match the worktree at the given paths.
///
/// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
/// Updates the index to match HEAD at the given paths.
///
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
fn commit(
&self,
message: &str,
name_and_email: Option<(&str, &str)>,
env: &HashMap<String, String>,
) -> Result<()>;
fn push(
&self,
@@ -203,6 +232,7 @@ pub trait GitRepository: Send + Sync {
upstream_name: &str,
options: Option<PushOptions>,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn pull(
@@ -210,8 +240,13 @@ pub trait GitRepository: Send + Sync {
branch_name: &str,
upstream_name: &str,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn fetch(
&self,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
@@ -242,19 +277,13 @@ impl std::fmt::Debug for dyn GitRepository {
pub struct RealGitRepository {
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
impl RealGitRepository {
pub fn new(
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
pub fn new(repository: git2::Repository, git_binary_path: Option<PathBuf>) -> Self {
Self {
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
hosting_provider_registry,
}
}
@@ -308,7 +337,7 @@ impl GitRepository for RealGitRepository {
Ok(details)
}
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()> {
let working_directory = self.working_directory()?;
let mode_flag = match mode {
@@ -317,6 +346,7 @@ impl GitRepository for RealGitRepository {
};
let output = new_std_command(&self.git_binary_path)
.envs(env)
.current_dir(&working_directory)
.args(["reset", mode_flag, commit])
.output()?;
@@ -329,7 +359,12 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
fn checkout_files(
&self,
commit: &str,
paths: &[RepoPath],
env: &HashMap<String, String>,
) -> Result<()> {
if paths.is_empty() {
return Ok(());
}
@@ -337,6 +372,7 @@ impl GitRepository for RealGitRepository {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["checkout", commit, "--"])
.args(paths.iter().map(|path| path.as_ref()))
.output()?;
@@ -385,11 +421,17 @@ impl GitRepository for RealGitRepository {
Some(content)
}
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
env: &HashMap<String, String>,
) -> anyhow::Result<()> {
let working_directory = self.working_directory()?;
if let Some(content) = content {
let mut child = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -402,6 +444,7 @@ impl GitRepository for RealGitRepository {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
.arg(path.as_ref())
.output()?;
@@ -415,6 +458,7 @@ impl GitRepository for RealGitRepository {
} else {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--force-remove"])
.arg(path.as_ref())
.output()?;
@@ -581,7 +625,6 @@ impl GitRepository for RealGitRepository {
path,
&content,
remote_url,
self.hosting_provider_registry.clone(),
)
}
@@ -607,12 +650,13 @@ impl GitRepository for RealGitRepository {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--add", "--remove", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
@@ -627,12 +671,13 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["reset", "--quiet", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
@@ -647,11 +692,17 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
fn commit(
&self,
message: &str,
name_and_email: Option<(&str, &str)>,
env: &HashMap<String, String>,
) -> Result<()> {
let working_directory = self.working_directory()?;
let mut cmd = new_std_command(&self.git_binary_path);
cmd.current_dir(&working_directory)
.envs(env)
.args(["commit", "--quiet", "-m"])
.arg(message)
.arg("--cleanup=strip");
@@ -677,11 +728,13 @@ impl GitRepository for RealGitRepository {
remote_name: &str,
options: Option<PushOptions>,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
@@ -705,11 +758,13 @@ impl GitRepository for RealGitRepository {
branch_name: &str,
remote_name: &str,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
@@ -724,11 +779,16 @@ impl GitRepository for RealGitRepository {
run_remote_command(ask_pass, git_process)
}
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
fn fetch(
&self,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
@@ -919,7 +979,12 @@ impl GitRepository for FakeGitRepository {
state.head_contents.get(path.as_ref()).cloned()
}
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
_env: &HashMap<String, String>,
) -> anyhow::Result<()> {
let mut state = self.state.lock();
if let Some(message) = state.simulated_index_write_error_message.clone() {
return Err(anyhow::anyhow!(message));
@@ -952,11 +1017,11 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
fn reset(&self, _: &str, _: ResetMode, _: &HashMap<String, String>) -> Result<()> {
unimplemented!()
}
fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
fn checkout_files(&self, _: &str, _: &[RepoPath], _: &HashMap<String, String>) -> Result<()> {
unimplemented!()
}
@@ -1042,15 +1107,20 @@ impl GitRepository for FakeGitRepository {
.cloned()
}
fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
fn stage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
unimplemented!()
}
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
fn unstage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
unimplemented!()
}
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
fn commit(
&self,
_message: &str,
_name_and_email: Option<(&str, &str)>,
_env: &HashMap<String, String>,
) -> Result<()> {
unimplemented!()
}
@@ -1060,6 +1130,7 @@ impl GitRepository for FakeGitRepository {
_remote: &str,
_options: Option<PushOptions>,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
@@ -1069,11 +1140,16 @@ impl GitRepository for FakeGitRepository {
_branch: &str,
_remote: &str,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
fn fetch(
&self,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}

View File

@@ -18,13 +18,12 @@ test-support = ["multi_buffer/test-support"]
[dependencies]
anyhow.workspace = true
askpass.workspace= true
askpass.workspace = true
buffer_diff.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
@@ -51,6 +50,7 @@ strum.workspace = true
telemetry.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
use crate::askpass_modal::AskPassModal;
use crate::commit_modal::CommitModal;
use crate::git_panel_settings::StatusStyle;
use crate::project_diff::Diff;
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
use crate::repository_selector::filtered_repository_entries;
use crate::{branch_picker, render_remote_button};
@@ -40,7 +41,8 @@ use language_model::{
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
use panel::{
panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, PanelHeader,
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button, PanelHeader,
};
use project::{
git::{GitEvent, Repository},
@@ -101,9 +103,14 @@ enum TrashCancel {
Cancel,
}
fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
fn git_panel_context_menu(
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.context(focus_handle)
.action("Stage All", StageAll.boxed_clone())
.action("Unstage All", UnstageAll.boxed_clone())
.separator()
@@ -231,6 +238,7 @@ pub struct GitPanel {
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
new_count: usize,
entry_count: usize,
new_staged_count: usize,
pending: Vec<PendingOperation>,
pending_commit: Option<Task<()>>,
@@ -381,6 +389,7 @@ impl GitPanel {
context_menu: None,
workspace,
modal_open: false,
entry_count: 0,
};
git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -685,10 +694,6 @@ impl GitPanel {
}
}
pub(crate) fn editor_focus_handle(&self, cx: &mut Context<Self>) -> FocusHandle {
self.commit_editor.focus_handle(cx).clone()
}
fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
self.commit_editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx));
@@ -741,6 +746,7 @@ impl GitPanel {
.as_ref()
{
project_diff.focus_handle(cx).focus(window);
project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
return None;
}
}
@@ -898,13 +904,14 @@ impl GitPanel {
let buffers = futures::future::join_all(tasks).await;
active_repository
.update(&mut cx, |repo, _| {
.update(&mut cx, |repo, cx| {
repo.checkout_files(
"HEAD",
entries
.iter()
.map(|entries| entries.repo_path.clone())
.collect(),
cx,
)
})?
.await??;
@@ -1078,7 +1085,7 @@ impl GitPanel {
});
}
fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
pub fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
let entries = self
.entries
.iter()
@@ -1089,7 +1096,7 @@ impl GitPanel {
self.change_file_stage(true, entries, cx);
}
fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
pub fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
let entries = self
.entries
.iter()
@@ -1228,6 +1235,35 @@ impl GitPanel {
}
}
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
};
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Staged {
self.change_file_stage(true, vec![status_entry.clone()], cx);
}
}
fn unstage_selected(
&mut self,
_: &git::UnstageFile,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
};
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Unstaged {
self.change_file_stage(false, vec![status_entry.clone()], cx);
}
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
if self
.commit_editor
@@ -1286,7 +1322,8 @@ impl GitPanel {
let task = if self.has_staged_changes() {
// Repository serializes all git operations, so we can just send a commit immediately
let commit_task = active_repository.read(cx).commit(message.into(), None);
let commit_task =
active_repository.update(cx, |repo, cx| repo.commit(message.into(), None, cx));
cx.background_spawn(async move { commit_task.await? })
} else {
let changed_files = self
@@ -1307,7 +1344,7 @@ impl GitPanel {
cx.spawn(|_, mut cx| async move {
stage_task.await?;
let commit_task = active_repository
.update(&mut cx, |repo, _| repo.commit(message.into(), None))?;
.update(&mut cx, |repo, cx| repo.commit(message.into(), None, cx))?;
commit_task.await?
})
};
@@ -1343,7 +1380,7 @@ impl GitPanel {
if let Ok(true) = confirmation.await {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
repo.update(&mut cx, |repo, cx| repo.reset("HEAD^", ResetMode::Soft, cx))?
.await??;
Ok(Some(prior_head))
@@ -1453,6 +1490,10 @@ impl GitPanel {
/// Generates a commit message using an LLM.
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
if !self.can_commit() {
return;
}
let model = match current_language_model(cx) {
Some(value) => value,
None => return,
@@ -2133,10 +2174,12 @@ impl GitPanel {
self.tracked_count = 0;
self.new_staged_count = 0;
self.tracked_staged_count = 0;
self.entry_count = 0;
for entry in &self.entries {
let Some(status_entry) = entry.status_entry() else {
continue;
};
self.entry_count += 1;
if repo.has_conflict(&status_entry.repo_path) {
self.conflicted_count += 1;
if self.entry_staging(status_entry).has_staged() {
@@ -2263,6 +2306,18 @@ impl GitPanel {
self.has_staged_changes()
}
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
.anchor(Corner::TopRight)
}
pub(crate) fn render_generate_commit_message_button(
&self,
cx: &Context<Self>,
@@ -2291,14 +2346,25 @@ impl GitPanel {
.into_any_element();
}
let can_commit = self.can_commit();
let editor_focus_handle = self.commit_editor.focus_handle(cx);
IconButton::new("generate-commit-message", IconName::AiEdit)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.tooltip(Tooltip::for_action_title_in(
"Generate Commit Message",
&git::GenerateCommitMessage,
&self.commit_editor.focus_handle(cx),
))
.tooltip(move |window, cx| {
if can_commit {
Tooltip::for_action_in(
"Generate Commit Message",
&git::GenerateCommitMessage,
&editor_focus_handle,
window,
cx,
)
} else {
Tooltip::simple("No changes to commit", cx)
}
})
.disabled(!can_commit)
.on_click(cx.listener(move |this, _event, _window, cx| {
this.generate_commit_message(cx);
}))
@@ -2345,10 +2411,7 @@ impl GitPanel {
if self.has_unstaged_conflicts() {
(false, "You must resolve conflicts before committing")
} else if !self.has_staged_changes() && !self.has_tracked_changes() {
(
false,
"You must have either staged changes or tracked files to commit",
)
(false, "No changes to commit")
} else if self.pending_commit.is_some() {
(false, "Commit in progress")
} else if self.custom_or_suggested_commit_message(cx).is_none() {
@@ -2384,6 +2447,61 @@ impl GitPanel {
})
}
fn render_panel_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let text;
let action;
let tooltip;
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
text = "Unstage All";
action = git::UnstageAll.boxed_clone();
tooltip = "git reset";
} else {
text = "Stage All";
action = git::StageAll.boxed_clone();
tooltip = "git add --all ."
}
let change_string = match self.entry_count {
0 => "No Changes".to_string(),
1 => "1 Change".to_string(),
_ => format!("{} Changes", self.entry_count),
};
self.panel_header_container(window, cx)
.px_2()
.child(
panel_button(change_string)
.color(Color::Muted)
.tooltip(Tooltip::for_action_title_in(
"Open diff",
&Diff,
&self.focus_handle,
))
.on_click(|_, _, cx| {
cx.defer(|cx| {
cx.dispatch_action(&Diff);
})
}),
)
.child(div().flex_grow()) // spacer
.child(self.render_overflow_menu("overflow_menu"))
.child(
panel_filled_button(text)
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.disabled(self.entry_count == 0)
.on_click(move |_, _, cx| {
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}),
)
}
pub fn render_footer(
&self,
window: &mut Window,
@@ -2526,15 +2644,14 @@ impl GitPanel {
.items_center()
.py_2()
.px(px(8.))
// .bg(cx.theme().colors().background)
// .border_t_1()
.border_color(cx.theme().colors().border)
.gap_1p5()
.child(
div()
.flex_grow()
.overflow_hidden()
.max_w(relative(0.6))
.items_center()
.max_w(relative(0.85))
.h_full()
.child(
Label::new(commit.subject.clone())
@@ -2824,6 +2941,7 @@ impl GitPanel {
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.context(self.focus_handle.clone())
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile.boxed_clone())
.separator()
@@ -2840,7 +2958,7 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_menu = git_panel_context_menu(window, cx);
let context_menu = git_panel_context_menu(self.focus_handle.clone(), window, cx);
self.set_context_menu(context_menu, position, window, cx);
}
@@ -2887,6 +3005,9 @@ impl GitPanel {
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
let status = entry.status;
let modifiers = self.current_modifiers;
let shift_held = modifiers.shift;
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
let is_deleted = status.is_deleted();
@@ -2972,7 +3093,7 @@ impl GitPanel {
.px(rems(0.75)) // ~12px
.overflow_hidden()
.flex_none()
.gap(DynamicSpacing::Base04.rems(cx))
.gap_1p5()
.bg(base_bg)
.hover(|this| this.bg(hover_bg))
.active(|this| this.bg(active_bg))
@@ -3017,6 +3138,7 @@ impl GitPanel {
.flex_none()
.occlude()
.cursor_pointer()
.ml_neg_0p5()
.child(
Checkbox::new(checkbox_id, is_staged)
.disabled(!has_write_access)
@@ -3038,17 +3160,35 @@ impl GitPanel {
})
})
.tooltip(move |window, cx| {
let tooltip_name = if entry_staging.is_fully_staged() {
"Unstage"
let is_staged = entry_staging.is_fully_staged();
let action = if is_staged { "Unstage" } else { "Stage" };
let tooltip_name = if shift_held {
format!("{} section", action)
} else {
"Stage"
action.to_string()
};
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
let meta = if shift_held {
format!(
"Release shift to {} single entry",
action.to_lowercase()
)
} else {
format!("Shift click to {} section", action.to_lowercase())
};
Tooltip::with_meta(
tooltip_name,
Some(&ToggleStaged),
meta,
window,
cx,
)
}),
),
)
.child(git_status_icon(status, cx))
.child(git_status_icon(status))
.child(
h_flex()
.items_center()
@@ -3107,10 +3247,16 @@ impl Render for GitPanel {
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(has_write_access && !project.is_read_only(cx), |this| {
this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
this.toggle_staged_for_selected(&ToggleStaged, window, cx)
}))
.on_action(cx.listener(GitPanel::commit))
this.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(GitPanel::commit))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::stage_selected))
.on_action(cx.listener(Self::unstage_selected))
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::generate_commit_message_action))
})
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
@@ -3119,16 +3265,9 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::close_panel))
.on_action(cx.listener(Self::open_diff))
.on_action(cx.listener(Self::open_file))
.on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::expand_commit_editor))
.on_action(cx.listener(Self::generate_commit_message_action))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@@ -3148,6 +3287,7 @@ impl Render for GitPanel {
.child(
v_flex()
.size_full()
.child(self.render_panel_header(window, cx))
.map(|this| {
if has_entries {
this.child(self.render_entries(has_write_access, window, cx))
@@ -3350,23 +3490,11 @@ impl PanelRepoFooter {
git_panel: None,
}
}
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
.anchor(Corner::TopRight)
}
}
impl RenderOnce for PanelRepoFooter {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let active_repo = self.active_repository.clone();
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
let repo_selector_trigger = Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
@@ -3446,6 +3574,7 @@ impl RenderOnce for PanelRepoFooter {
.h(px(36.))
.items_center()
.justify_between()
.gap_1()
.child(
h_flex()
.flex_1()
@@ -3455,7 +3584,11 @@ impl RenderOnce for PanelRepoFooter {
div().child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
.color(if single_repo {
Color::Disabled
} else {
Color::Muted
}),
),
)
.child(repo_selector)
@@ -3474,7 +3607,6 @@ impl RenderOnce for PanelRepoFooter {
.gap_1()
.flex_shrink_0()
.children(spinner)
.child(self.render_overflow_menu(overflow_menu_id))
.when_some(branch, |this, branch| {
let mut focus_handle = None;
if let Some(git_panel) = self.git_panel.as_ref() {

View File

@@ -1,13 +1,13 @@
use ::settings::Settings;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use git_panel_settings::GitPanelSettings;
use gpui::{App, Entity, FocusHandle};
use project::Project;
use project_diff::ProjectDiff;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString};
use ui::prelude::*;
use workspace::Workspace;
mod askpass_modal;
@@ -29,69 +29,65 @@ pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, cx| {
let project = workspace.project().read(cx);
if project.is_via_collab() {
if project.is_read_only(cx) {
return;
}
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
if !project.is_via_collab() {
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
});
});
}
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(window, cx);
panel.stage_all(action, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
panel.unstage_all(action, window, cx);
});
});
})
.detach();
}
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
GitStatusIcon::new(status)
}
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
@@ -157,6 +153,7 @@ mod remote_button {
0,
0,
Some(IconName::ArrowCircle),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Fetch), cx);
},
@@ -184,6 +181,7 @@ mod remote_button {
ahead as usize,
0,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
@@ -212,6 +210,7 @@ mod remote_button {
ahead as usize,
behind as usize,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Pull), cx);
},
@@ -238,6 +237,7 @@ mod remote_button {
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
@@ -264,6 +264,7 @@ mod remote_button {
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
@@ -305,7 +306,10 @@ mod remote_button {
}
}
fn render_git_action_menu(id: impl Into<ElementId>) -> impl IntoElement {
fn render_git_action_menu(
id: impl Into<ElementId>,
keybinding_target: Option<FocusHandle>,
) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("split-button-right")
@@ -320,6 +324,9 @@ mod remote_button {
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
el.context(keybinding_target.clone())
})
.action("Fetch", git::Fetch.boxed_clone())
.action("Pull", git::Pull.boxed_clone())
.separator()
@@ -337,12 +344,14 @@ mod remote_button {
}
impl SplitButton {
#[allow(clippy::too_many_arguments)]
fn new(
id: impl Into<SharedString>,
left_label: impl Into<SharedString>,
ahead_count: usize,
behind_count: usize,
left_icon: Option<IconName>,
keybinding_target: Option<FocusHandle>,
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
@@ -400,9 +409,10 @@ mod remote_button {
.on_click(left_on_click)
.tooltip(tooltip);
let right = render_git_action_menu(ElementId::Name(
format!("split-button-right-{}", id).into(),
))
let right = render_git_action_menu(
ElementId::Name(format!("split-button-right-{}", id).into()),
keybinding_target,
)
.into_any_element();
Self { left, right }
@@ -433,3 +443,79 @@ mod remote_button {
}
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct GitStatusIcon {
status: FileStatus,
}
impl GitStatusIcon {
pub fn new(status: FileStatus) -> Self {
Self { status }
}
}
impl RenderOnce for GitStatusIcon {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
let status = self.status;
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
worktree_status: code,
})
}
let modified = tracked_file_status(StatusCode::Modified);
let added = tracked_file_status(StatusCode::Added);
let deleted = tracked_file_status(StatusCode::Deleted);
let conflict = UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into();
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
}
}

View File

@@ -7,7 +7,6 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent,
};
use feature_flags::FeatureFlagViewExt;
use futures::StreamExt;
use git::{
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
@@ -64,16 +63,13 @@ const NEW_NAMESPACE: &'static str = "2";
impl ProjectDiff {
pub(crate) fn register(
_: &mut Workspace,
window: Option<&mut Window>,
workspace: &mut Workspace,
_window: Option<&mut Window>,
cx: &mut Context<Workspace>,
) {
let Some(window) = window else { return };
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
workspace.register_action(Self::deploy);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
});
workspace.register_action(Self::deploy);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
});
workspace::register_serializable_item::<ProjectDiff>(cx);
@@ -125,6 +121,12 @@ impl ProjectDiff {
}
}
pub fn autoscroll(&self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx);
})
}
fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
@@ -472,7 +474,10 @@ impl ProjectDiff {
})?;
}
}
this.update(&mut cx, |this, _| this.pending_scroll.take())?;
this.update(&mut cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
})?;
}
Ok(())

View File

@@ -152,7 +152,10 @@ impl GoToLine {
cx: &mut Context<Self>,
) {
match event {
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
editor::EditorEvent::Blurred => {
self.prev_scroll_position.take();
cx.emit(DismissEvent)
}
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}

View File

@@ -207,7 +207,7 @@ blade-macros.workspace = true
flume = "0.11"
rand.workspace = true
windows.workspace = true
windows-core = "0.58"
windows-core = "0.60"
[dev-dependencies]
backtrace = "0.3"

View File

@@ -178,6 +178,7 @@ impl EntityMap {
}
}
#[track_caller]
fn double_lease_panic<T>(operation: &str) -> ! {
panic!(
"cannot {operation} {} while it is already being updated",

View File

@@ -683,11 +683,11 @@ impl Default for Background {
}
/// Creates a hash pattern background
pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
pub fn pattern_slash(color: Hsla, height: f32) -> Background {
Background {
tag: BackgroundTag::PatternSlash,
solid: color,
gradient_angle_or_pattern_height: thickness,
gradient_angle_or_pattern_height: height,
..Default::default()
}
}

View File

@@ -76,8 +76,9 @@ impl Keystroke {
}
/// key syntax is:
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
/// key_char syntax is only used for generating test events,
/// secondary means "cmd" on macOS and "ctrl" on other platforms
/// when matching a key with an key_char set will be matched without it.
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut control = false;
@@ -95,6 +96,13 @@ impl Keystroke {
"alt" => alt = true,
"shift" => shift = true,
"fn" => function = true,
"secondary" => {
if cfg!(target_os = "macos") {
platform = true
} else {
control = true
};
}
"cmd" | "super" | "win" => platform = true,
_ => {
if let Some(next) = components.peek() {

View File

@@ -115,7 +115,6 @@ pub struct WaylandWindowStatePtr {
}
impl WaylandWindowState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
handle: AnyWindowHandle,
surface: wl_surface::WlSurface,

View File

@@ -353,7 +353,6 @@ where
}
impl X11WindowState {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,
@@ -712,7 +711,6 @@ enum WmHintPropertyState {
}
impl X11Window {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,

View File

@@ -347,20 +347,7 @@ impl MacPlatform {
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
}
item.setKeyEquivalentModifierMask_(mask);
}
// For multi-keystroke bindings, render the keystroke as part of the title.
else {
use std::fmt::Write;
let mut name = format!("{name} [");
for (i, keystroke) in keystrokes.iter().enumerate() {
if i > 0 {
name.push(' ');
}
write!(&mut name, "{}", keystroke).unwrap();
}
name.push(']');
} else {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(&name),

View File

@@ -268,6 +268,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
@@ -334,6 +338,7 @@ struct MacWindowState {
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
transparent_titlebar: bool,
previous_modifiers_changed_event: Option<PlatformInput>,
keystroke_for_do_command: Option<Keystroke>,
do_command_handled: Option<bool>,
@@ -613,6 +618,9 @@ impl MacWindow {
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
.map_or(true, |titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
@@ -1490,6 +1498,19 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.fullscreen_restore_bounds = lock.bounds();
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(NO);
}
}
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if lock.transparent_titlebar {
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(YES);
}
}
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {

View File

@@ -170,7 +170,7 @@ fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
let handle = GlobalLock(global);
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
let _ = GlobalUnlock(global);
SetClipboardData(format, HANDLE(global.0))?;
SetClipboardData(format, Some(HANDLE(global.0)))?;
}
Ok(())
}

View File

@@ -1049,7 +1049,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_measuringmode: DWRITE_MEASURING_MODE,
glyphrun: *const DWRITE_GLYPH_RUN,
glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
) -> windows::core::Result<()> {
unsafe {
let glyphrun = &*glyphrun;
@@ -1113,7 +1113,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_baselineoriginx: f32,
_baselineoriginy: f32,
_underline: *const DWRITE_UNDERLINE,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,
@@ -1127,7 +1127,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_baselineoriginx: f32,
_baselineoriginy: f32,
_strikethrough: *const DWRITE_STRIKETHROUGH,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,
@@ -1140,10 +1140,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_clientdrawingcontext: *const ::core::ffi::c_void,
_originx: f32,
_originy: f32,
_inlineobject: Option<&IDWriteInlineObject>,
_inlineobject: windows::core::Ref<IDWriteInlineObject>,
_issideways: BOOL,
_isrighttoleft: BOOL,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,

View File

@@ -215,7 +215,7 @@ fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
unsafe {
EnumDisplayMonitors(
HDC::default(),
None,
None,
Some(monitor_enum_proc),
LPARAM(&mut monitors as *mut _ as _),

View File

@@ -177,7 +177,12 @@ fn handle_size_msg(
fn handle_size_move_loop(handle: HWND) -> Option<isize> {
unsafe {
let ret = SetTimer(handle, SIZE_MOVE_LOOP_TIMER_ID, USER_TIMER_MINIMUM, None);
let ret = SetTimer(
Some(handle),
SIZE_MOVE_LOOP_TIMER_ID,
USER_TIMER_MINIMUM,
None,
);
if ret == 0 {
log::error!(
"unable to create timer: {}",
@@ -190,7 +195,7 @@ fn handle_size_move_loop(handle: HWND) -> Option<isize> {
fn handle_size_move_loop_exit(handle: HWND) -> Option<isize> {
unsafe {
KillTimer(handle, SIZE_MOVE_LOOP_TIMER_ID).log_err();
KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err();
}
None
}
@@ -217,7 +222,7 @@ fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Optio
request_frame(Default::default());
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
}
unsafe { ValidateRect(handle, None).ok().log_err() };
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
Some(0)
}
@@ -776,7 +781,7 @@ fn handle_activate_msg(
if state_ptr.hide_title_bar {
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
unsafe {
InvalidateRect(handle, Some(&titlebar_rect), FALSE)
InvalidateRect(Some(handle), Some(&titlebar_rect), false)
.ok()
.log_err()
};
@@ -1105,7 +1110,7 @@ fn handle_nc_mouse_up_msg(
HTCLOSE => {
if last_button == HTCLOSE {
unsafe {
PostMessageW(handle, WM_CLOSE, WPARAM::default(), LPARAM::default())
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
.log_err()
};
handled = true;
@@ -1133,7 +1138,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
) {
return None;
}
unsafe { SetCursor(state_ptr.state.borrow().current_cursor) };
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
Some(1)
}

View File

@@ -130,14 +130,9 @@ impl WindowsPlatform {
fn redraw_all(&self) {
for handle in self.raw_window_handles.read().iter() {
unsafe {
RedrawWindow(
*handle,
None,
HRGN::default(),
RDW_INVALIDATE | RDW_UPDATENOW,
)
.ok()
.log_err();
RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW)
.ok()
.log_err();
}
}
}
@@ -156,7 +151,7 @@ impl WindowsPlatform {
.read()
.iter()
.for_each(|handle| unsafe {
PostMessageW(*handle, message, wparam, lparam).log_err();
PostMessageW(Some(*handle), message, wparam, lparam).log_err();
});
}
@@ -620,7 +615,7 @@ impl Platform for WindowsPlatform {
CredReadW(
PCWSTR::from_raw(target_name.as_ptr()),
CRED_TYPE_GENERIC,
0,
None,
&mut credentials,
)?
};
@@ -648,7 +643,13 @@ impl Platform for WindowsPlatform {
.chain(Some(0))
.collect_vec();
self.foreground_executor().spawn(async move {
unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };
unsafe {
CredDeleteW(
PCWSTR::from_raw(target_name.as_ptr()),
CRED_TYPE_GENERIC,
None,
)?
};
Ok(())
})
}
@@ -805,7 +806,7 @@ fn load_icon() -> Result<HICON> {
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
let handle = unsafe {
LoadImageW(
module,
Some(module.into()),
windows::core::PCWSTR(1 as _),
IMAGE_ICON,
0,

View File

@@ -2,6 +2,7 @@ use std::sync::OnceLock;
use ::util::ResultExt;
use windows::{
core::BOOL,
Wdk::System::SystemServices::RtlGetVersion,
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
UI::{

View File

@@ -296,7 +296,7 @@ impl WindowsWindowStatePtr {
unsafe {
SetWindowPos(
state_ptr.hwnd,
HWND::default(),
None,
x,
y,
cx,
@@ -433,7 +433,7 @@ impl WindowsWindow {
CW_USEDEFAULT,
None,
None,
hinstance,
Some(hinstance.into()),
lpparam,
)
};
@@ -650,7 +650,7 @@ impl PlatformWindow for WindowsWindow {
.spawn(async move {
this.set_window_placement().log_err();
unsafe { SetActiveWindow(hwnd).log_err() };
unsafe { SetFocus(hwnd).log_err() };
unsafe { SetFocus(Some(hwnd)).log_err() };
// todo(windows)
// crate `windows 0.56` reports true as Err
unsafe { SetForegroundWindow(hwnd).as_bool() };
@@ -817,16 +817,13 @@ impl WindowsDragDropHandler {
impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
fn DragEnter(
&self,
pdataobj: Option<&IDataObject>,
pdataobj: windows::core::Ref<IDataObject>,
_grfkeystate: MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdweffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
unsafe {
let Some(idata_obj) = pdataobj else {
log::info!("no dragging file or directory detected");
return Ok(());
};
let idata_obj = pdataobj.ok()?;
let config = FORMATETC {
cfFormat: CF_HDROP.0,
ptd: std::ptr::null_mut() as _,
@@ -905,7 +902,7 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
fn Drop(
&self,
_pdataobj: Option<&IDataObject>,
_pdataobj: windows::core::Ref<IDataObject>,
_grfkeystate: MODIFIERKEYS_FLAGS,
pt: &POINTL,
_pdweffect: *mut DROPEFFECT,

View File

@@ -81,6 +81,29 @@ impl ShapedLine {
Ok(())
}
/// Paint the background of the line to the window.
pub fn paint_background(
&self,
origin: Point<Pixels>,
line_height: Pixels,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_line_background(
origin,
&self.layout,
line_height,
TextAlign::default(),
None,
&self.decoration_runs,
&[],
window,
cx,
)?;
Ok(())
}
}
/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
@@ -132,7 +155,6 @@ impl WrappedLine {
}
}
#[allow(clippy::too_many_arguments)]
fn paint_line(
origin: Point<Pixels>,
layout: &LineLayout,
@@ -160,7 +182,6 @@ fn paint_line(
let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone();
let mut glyph_origin = point(
aligned_origin_x(
@@ -183,21 +204,6 @@ fn paint_line(
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
if let Some((background_origin, background_color)) = current_background.as_mut()
{
if glyph_origin.x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
}
window.paint_quad(fill(
Bounds {
origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
*background_color,
));
background_origin.x = origin.x;
background_origin.y += line_height;
}
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
if glyph_origin.x == underline_origin.x {
underline_origin.x -= max_glyph_size.width.half();
@@ -237,7 +243,6 @@ fn paint_line(
}
prev_glyph_position = glyph.position;
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
if glyph.index >= run_end {
@@ -253,18 +258,6 @@ fn paint_line(
}
if let Some(style_run) = style_run {
if let Some((_, background_color)) = &mut current_background {
if style_run.background_color.as_ref() != Some(background_color) {
finished_background = current_background.take();
}
}
if let Some(run_background) = style_run.background_color {
current_background.get_or_insert((
point(glyph_origin.x, glyph_origin.y),
run_background,
));
}
if let Some((_, underline_style)) = &mut current_underline {
if style_run.underline.as_ref() != Some(underline_style) {
finished_underline = current_underline.take();
@@ -306,26 +299,11 @@ fn paint_line(
color = style_run.color;
} else {
run_end = layout.len;
finished_background = current_background.take();
finished_underline = current_underline.take();
finished_strikethrough = current_strikethrough.take();
}
}
if let Some((mut background_origin, background_color)) = finished_background {
let mut width = glyph_origin.x - background_origin.x;
if background_origin.x == glyph_origin.x {
background_origin.x -= max_glyph_size.width.half();
};
window.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(width, line_height),
},
background_color,
));
}
if let Some((mut underline_origin, underline_style)) = finished_underline {
if underline_origin.x == glyph_origin.x {
underline_origin.x -= max_glyph_size.width.half();
@@ -384,19 +362,6 @@ fn paint_line(
last_line_end_x -= glyph.position.x;
}
if let Some((mut background_origin, background_color)) = current_background.take() {
if last_line_end_x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
};
window.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height),
},
background_color,
));
}
if let Some((mut underline_start, underline_style)) = current_underline.take() {
if last_line_end_x == underline_start.x {
underline_start.x -= max_glyph_size.width.half()
@@ -423,6 +388,141 @@ fn paint_line(
})
}
fn paint_line_background(
origin: Point<Pixels>,
layout: &LineLayout,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
window: &mut Window,
cx: &mut App,
) -> Result<()> {
let line_bounds = Bounds::new(
origin,
size(
layout.width,
line_height * (wrap_boundaries.len() as f32 + 1.),
),
);
window.paint_layer(line_bounds, |window| {
let mut decoration_runs = decoration_runs.iter();
let mut wraps = wrap_boundaries.iter().peekable();
let mut run_end = 0;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone();
let mut glyph_origin = point(
aligned_origin_x(
origin,
align_width.unwrap_or(layout.width),
px(0.0),
&align,
layout,
wraps.peek(),
),
origin.y,
);
let mut prev_glyph_position = Point::default();
let mut max_glyph_size = size(px(0.), px(0.));
for (run_ix, run) in layout.runs.iter().enumerate() {
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
if let Some((background_origin, background_color)) = current_background.as_mut()
{
if glyph_origin.x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
}
window.paint_quad(fill(
Bounds {
origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
*background_color,
));
background_origin.x = origin.x;
background_origin.y += line_height;
}
}
prev_glyph_position = glyph.position;
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
if glyph.index >= run_end {
let mut style_run = decoration_runs.next();
// ignore style runs that apply to a partial glyph
while let Some(run) = style_run {
if glyph.index < run_end + (run.len as usize) {
break;
}
run_end += run.len as usize;
style_run = decoration_runs.next();
}
if let Some(style_run) = style_run {
if let Some((_, background_color)) = &mut current_background {
if style_run.background_color.as_ref() != Some(background_color) {
finished_background = current_background.take();
}
}
if let Some(run_background) = style_run.background_color {
current_background.get_or_insert((
point(glyph_origin.x, glyph_origin.y),
run_background,
));
}
run_end += style_run.len as usize;
} else {
run_end = layout.len;
finished_background = current_background.take();
}
}
if let Some((mut background_origin, background_color)) = finished_background {
let mut width = glyph_origin.x - background_origin.x;
if background_origin.x == glyph_origin.x {
background_origin.x -= max_glyph_size.width.half();
};
window.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(width, line_height),
},
background_color,
));
}
}
}
let mut last_line_end_x = origin.x + layout.width;
if let Some(boundary) = wrap_boundaries.last() {
let run = &layout.runs[boundary.run_ix];
let glyph = &run.glyphs[boundary.glyph_ix];
last_line_end_x -= glyph.position.x;
}
if let Some((mut background_origin, background_color)) = current_background.take() {
if last_line_end_x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
};
window.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height),
},
background_color,
));
}
Ok(())
})
}
fn aligned_origin_x(
origin: Point<Pixels>,
align_width: Pixels,

View File

@@ -1527,6 +1527,7 @@ impl Buffer {
}
fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut Context<Self>) {
self.was_changed();
self.non_text_state_update_count += 1;
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
@@ -1968,7 +1969,12 @@ impl Buffer {
/// This allows downstream code to check if the buffer's text has changed without
/// waiting for an effect cycle, which would be required if using eents.
pub fn record_changes(&mut self, bit: rc::Weak<Cell<bool>>) {
self.change_bits.push(bit);
if let Err(ix) = self
.change_bits
.binary_search_by_key(&rc::Weak::as_ptr(&bit), rc::Weak::as_ptr)
{
self.change_bits.insert(ix, bit);
}
}
fn was_changed(&mut self) {
@@ -2273,12 +2279,13 @@ impl Buffer {
}
fn did_edit(&mut self, old_version: &clock::Global, was_dirty: bool, cx: &mut Context<Self>) {
self.was_changed();
if self.edits_since::<usize>(old_version).next().is_none() {
return;
}
self.reparse(cx);
cx.emit(BufferEvent::Edited);
if was_dirty != self.is_dirty() {
cx.emit(BufferEvent::DirtyChanged);
@@ -2390,7 +2397,6 @@ impl Buffer {
}
self.text.apply_ops(buffer_ops);
self.deferred_ops.insert(deferred_ops);
self.was_changed();
self.flush_deferred_ops(cx);
self.did_edit(&old_version, was_dirty, cx);
// Notify independently of whether the buffer was edited as the operations could include a

View File

@@ -1250,7 +1250,6 @@ fn parse_text(
})
}
#[allow(clippy::too_many_arguments)]
fn get_injections(
config: &InjectionConfig,
text: &BufferSnapshot,

View File

@@ -11,8 +11,8 @@ use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, StreamExt};
use gpui::{
percentage, svg, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription,
Task, Transformation,
percentage, svg, Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render,
Subscription, Task, Transformation,
};
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
@@ -337,9 +337,20 @@ impl Render for ConfigurationView {
if self.state.read(cx).is_authenticated(cx) {
const LABEL: &str = "Authorized.";
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(LABEL))
.justify_between()
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(LABEL)),
)
.child(
Button::new("sign_out", "Sign Out")
.style(ui::ButtonStyle::Filled)
.on_click(|_, window, cx| {
window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
}),
)
} else {
let loading_icon = svg()
.size_8()

View File

@@ -1,6 +1,6 @@
name = "C++"
grammar = "cpp"
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh", "C", "H"]
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"]
line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [

View File

@@ -62,8 +62,8 @@
; Literals
(this) @keyword
(super) @keyword
(this) @variable.special
(super) @variable.special
[
(null)

View File

@@ -62,8 +62,8 @@
; Literals
(this) @keyword
(super) @keyword
(this) @variable.special
(super) @variable.special
[
(null)

View File

@@ -80,8 +80,8 @@
; Literals
(this) @keyword
(super) @keyword
(this) @variable.special
(super) @variable.special
[
(null)

View File

@@ -301,7 +301,6 @@ pub struct AdapterServerCapabilities {
impl LanguageServer {
/// Starts a language server process.
#[allow(clippy::too_many_arguments)]
pub fn new(
stderr_capture: Arc<Mutex<Option<String>>>,
server_id: LanguageServerId,
@@ -372,7 +371,6 @@ impl LanguageServer {
Ok(server)
}
#[allow(clippy::too_many_arguments)]
fn new_internal<Stdin, Stdout, Stderr, F>(
server_id: LanguageServerId,
server_name: LanguageServerName,

View File

@@ -2982,7 +2982,6 @@ impl MultiBuffer {
snapshot.check_invariants();
}
#[allow(clippy::too_many_arguments)]
fn recompute_diff_transforms_for_edit(
&self,
edit: &Edit<TypedOffset<Excerpt>>,
@@ -4176,6 +4175,9 @@ impl MultiBufferSnapshot {
let region = cursor.region()?;
let overshoot = offset - region.range.start;
let buffer_offset = region.buffer_range.start + overshoot;
if buffer_offset > region.buffer.len() {
return None;
}
Some((region.buffer, buffer_offset))
}
@@ -4184,8 +4186,11 @@ impl MultiBufferSnapshot {
cursor.seek(&point);
let region = cursor.region()?;
let overshoot = point - region.range.start;
let buffer_offset = region.buffer_range.start + overshoot;
Some((region.buffer, buffer_offset, region.is_main_buffer))
let buffer_point = region.buffer_range.start + overshoot;
if buffer_point > region.buffer.max_point() {
return None;
}
Some((region.buffer, buffer_point, region.is_main_buffer))
}
pub fn suggested_indents(

View File

@@ -3378,6 +3378,17 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
}
}
}
let point = snapshot.max_point();
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
return;
};
assert!(offset <= buffer.len(),);
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
return;
};
assert!(point <= buffer.max_point(),);
}
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {

View File

@@ -2360,7 +2360,6 @@ impl OutlinePanel {
)
}
#[allow(clippy::too_many_arguments)]
fn render_search_match(
&mut self,
multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
@@ -2452,7 +2451,6 @@ impl OutlinePanel {
))
}
#[allow(clippy::too_many_arguments)]
fn entry_element(
&self,
rendered_entry: PanelEntry,
@@ -3836,7 +3834,6 @@ impl OutlinePanel {
})
}
#[allow(clippy::too_many_arguments)]
fn push_entry(
&self,
state: &mut GenerationState,
@@ -4054,7 +4051,6 @@ impl OutlinePanel {
update_cached_entries
}
#[allow(clippy::too_many_arguments)]
fn add_excerpt_entries(
&self,
state: &mut GenerationState,
@@ -4113,7 +4109,6 @@ impl OutlinePanel {
}
}
#[allow(clippy::too_many_arguments)]
fn add_search_entries(
&mut self,
state: &mut GenerationState,

View File

@@ -18,8 +18,6 @@ pub trait PanelHeader: workspace::Panel {
.w_full()
.px_1()
.flex_none()
.border_b_1()
.border_color(cx.theme().colors().border)
}
}

View File

@@ -96,8 +96,8 @@ pub trait PickerDelegate: Sized + 'static {
None
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
"No matches".into()
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("No matches".into())
}
fn update_matches(
&mut self,
@@ -844,18 +844,17 @@ impl<D: PickerDelegate> Render for Picker<D> {
)
})
.when(self.delegate.match_count() == 0, |el| {
el.child(
v_flex().flex_grow().py_2().child(
ListItem::new("empty_state")
.inset(true)
.spacing(ListItemSpacing::Sparse)
.disabled(true)
.child(
Label::new(self.delegate.no_matches_text(window, cx))
.color(Color::Muted),
),
),
)
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
el.child(
v_flex().flex_grow().py_2().child(
ListItem::new("empty_state")
.inset(true)
.spacing(ListItemSpacing::Sparse)
.disabled(true)
.child(Label::new(text).color(Color::Muted)),
),
)
})
})
.children(self.delegate.render_footer(window, cx))
.children(match &self.head {

View File

@@ -6,7 +6,7 @@ use crate::{
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, bail, Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffEvent};
use buffer_diff::BufferDiff;
use client::Client;
use collections::{hash_map, HashMap, HashSet};
use fs::Fs;
@@ -15,7 +15,6 @@ use git::{blame::Blame, repository::RepoPath};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
};
use http_client::Url;
use language::{
proto::{
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
@@ -34,7 +33,6 @@ use std::{
ops::Range,
path::{Path, PathBuf},
pin::pin,
str::FromStr as _,
sync::Arc,
time::Instant,
};
@@ -217,39 +215,29 @@ impl BufferDiffState {
_ => false,
};
self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
let mut unstaged_changed_range = None;
let mut new_unstaged_diff = None;
if let Some(unstaged_diff) = &unstaged_diff {
unstaged_changed_range = BufferDiff::update_diff(
unstaged_diff.clone(),
buffer.clone(),
index,
index_changed,
language_changed,
language.clone(),
language_registry.clone(),
&mut cx,
)
.await?;
unstaged_diff.update(&mut cx, |_, cx| {
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
if let Some(changed_range) = unstaged_changed_range.clone() {
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
})
}
})?;
new_unstaged_diff = Some(
BufferDiff::update_diff(
unstaged_diff.clone(),
buffer.clone(),
index,
index_changed,
language_changed,
language.clone(),
language_registry.clone(),
&mut cx,
)
.await?,
);
}
let mut new_uncommitted_diff = None;
if let Some(uncommitted_diff) = &uncommitted_diff {
let uncommitted_changed_range =
if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx)
})?
} else {
new_uncommitted_diff = if index_matches_head {
new_unstaged_diff.clone()
} else {
Some(
BufferDiff::update_diff(
uncommitted_diff.clone(),
buffer.clone(),
@@ -260,32 +248,32 @@ impl BufferDiffState {
language_registry.clone(),
&mut cx,
)
.await?
};
.await?,
)
}
}
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{
unstaged_diff.update(&mut cx, |diff, cx| {
diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
})?
} else {
None
};
if let Some((uncommitted_diff, new_uncommitted_diff)) =
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
{
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
(None, None) => None,
(Some(unstaged_range), None) => {
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
}
(None, Some(uncommitted_range)) => Some(uncommitted_range),
(Some(unstaged_range), Some(uncommitted_range)) => {
let mut start = uncommitted_range.start;
let mut end = uncommitted_range.end;
if let Some(unstaged_range) =
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
{
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
}
Some(start..end)
}
};
cx.emit(BufferDiffEvent::DiffChanged { changed_range });
uncommitted_diff.set_snapshot(
&buffer,
new_uncommitted_diff,
language_changed,
unstaged_changed_range,
cx,
);
})?;
}
@@ -813,8 +801,7 @@ impl LocalBufferStore {
let Some(buffer) = buffer.upgrade() else {
continue;
};
let buffer = buffer.read(cx);
let Some(file) = File::from_dyn(buffer.file()) else {
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
continue;
};
if file.worktree != worktree_handle {
@@ -825,7 +812,6 @@ impl LocalBufferStore {
.iter()
.any(|(work_dir, _)| file.path.starts_with(work_dir))
{
let snapshot = buffer.text_snapshot();
let has_unstaged_diff = diff_state
.unstaged_diff
.as_ref()
@@ -835,7 +821,7 @@ impl LocalBufferStore {
.as_ref()
.is_some_and(|set| set.is_upgradable());
diff_state_updates.push((
snapshot.clone(),
buffer,
file.path.clone(),
has_unstaged_diff.then(|| diff_state.index_text.clone()),
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
@@ -854,36 +840,33 @@ impl LocalBufferStore {
.background_spawn(async move {
diff_state_updates
.into_iter()
.filter_map(
|(buffer_snapshot, path, current_index_text, current_head_text)| {
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let index_text = if current_index_text.is_some() {
local_repo.repo().load_index_text(&relative_path)
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo.repo().load_committed_text(&relative_path)
} else {
None
};
.filter_map(|(buffer, path, current_index_text, current_head_text)| {
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let index_text = if current_index_text.is_some() {
local_repo.repo().load_index_text(&relative_path)
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo.repo().load_committed_text(&relative_path)
} else {
None
};
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
return None;
}
return None;
}
}
let diff_bases_change = match (
current_index_text.is_some(),
current_head_text.is_some(),
) {
let diff_bases_change =
match (current_index_text.is_some(), current_head_text.is_some()) {
(true, true) => Some(if index_text == head_text {
DiffBasesChange::SetBoth(head_text)
} else {
@@ -896,17 +879,17 @@ impl LocalBufferStore {
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
(false, false) => None,
};
Some((buffer_snapshot, diff_bases_change))
},
)
Some((buffer, diff_bases_change))
})
.collect::<Vec<_>>()
})
.await;
this.update(&mut cx, |this, cx| {
for (buffer_snapshot, diff_bases_change) in diff_bases_changes_by_buffer {
for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
let Some(OpenBuffer::Complete { diff_state, .. }) =
this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
else {
continue;
};
@@ -917,8 +900,9 @@ impl LocalBufferStore {
diff_state.update(cx, |diff_state, cx| {
use proto::update_diff_bases::Mode;
let buffer = buffer.read(cx);
if let Some((client, project_id)) = this.downstream_client.as_ref() {
let buffer_id = buffer_snapshot.remote_id().to_proto();
let buffer_id = buffer.remote_id().to_proto();
let (staged_text, committed_text, mode) = match diff_bases_change
.clone()
{
@@ -942,8 +926,11 @@ impl LocalBufferStore {
client.send(message).log_err();
}
let _ =
diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
let _ = diff_state.diff_bases_changed(
buffer.text_snapshot(),
diff_bases_change,
cx,
);
});
}
})
@@ -1705,11 +1692,17 @@ impl BufferStore {
Err(e) => return Task::ready(Err(e)),
};
let remote = repo_entry
.branch()
.and_then(|b| b.upstream.as_ref())
.and_then(|b| b.remote_name())
.unwrap_or("origin")
.to_string();
cx.spawn(|cx| async move {
const REMOTE_NAME: &str = "origin";
let origin_url = repo
.remote_url(REMOTE_NAME)
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
.remote_url(&remote)
.ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
let sha = repo
.head_sha()
@@ -2787,20 +2780,10 @@ fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::B
})
.collect::<Vec<_>>();
let permalinks = blame
.permalinks
.into_iter()
.map(|(oid, url)| proto::CommitPermalink {
oid: oid.as_bytes().into(),
permalink: url.to_string(),
})
.collect::<Vec<_>>();
proto::BlameBufferResponse {
blame_response: Some(proto::blame_buffer_response::BlameResponse {
entries,
messages,
permalinks,
remote_url: blame.remote_url,
}),
}
@@ -2839,20 +2822,8 @@ fn deserialize_blame_buffer_response(
.filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
.collect::<HashMap<_, _>>();
let permalinks = response
.permalinks
.into_iter()
.filter_map(|permalink| {
Some((
git::Oid::from_bytes(&permalink.oid).ok()?,
Url::from_str(&permalink.permalink).ok()?,
))
})
.collect::<HashMap<_, _>>();
Some(Blame {
entries,
permalinks,
messages,
remote_url: response.remote_url,
})

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