Compare commits

..

142 Commits

Author SHA1 Message Date
Nate Butler
0ae34d98ad Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
392f597966 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c3e801c586 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
86c4faa12f Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
b545a02125 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
4762103c2a Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
a982ff848f Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
416e940bdd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
6899dab525 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
7f28b16825 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
196941a310 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
1e82854b7e Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
cc81f91dfb Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
be633875a5 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
ff28aed411 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
0bc287b863 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
d325d60a21 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
9b8bc2a2cd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
57978ece43 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
31de418b58 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
3376d63f57 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
87e49e4d91 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
2727be4df9 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
7498f508f1 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
0ebe6f78cf Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c327d9d3f4 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
60f6fe3454 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c47d805806 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
14552268fd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
23eb000abe panel 2024-12-11 11:10:03 -05:00
Nate Butler
7abe14dba8 wip 2024-12-11 11:09:03 -05:00
Nate Butler
f0c4643bbd Revert "wip - broken"
This reverts commit c09cf6eee0039f0b43d6cc7d5087646c842f984e.
2024-12-11 11:09:03 -05:00
Nate Butler
6883907163 wip - broken 2024-12-11 11:09:03 -05:00
Nate Butler
49a62b6414 Update git_ui.rs 2024-12-11 11:09:02 -05:00
Nate Butler
160b65771c headers in list 2024-12-11 11:09:02 -05:00
Nate Butler
214e997b72 Organize 2024-12-11 11:09:02 -05:00
Nate Butler
825a353228 Update git_ui.rs 2024-12-11 11:09:02 -05:00
Nate Butler
310cc3e315 add kb movement 2024-12-11 11:09:02 -05:00
Nate Butler
75ff03e45b wip basic list 2024-12-11 11:09:02 -05:00
Nate Butler
eb95efb0f3 clean up unused 2024-12-11 11:09:02 -05:00
Nate Butler
059ebd88fe new icons 2024-12-11 11:09:02 -05:00
Nate Butler
e8435a8915 Update git_ui.rs 2024-12-11 11:07:12 -05:00
Nate Butler
e41e3f8463 wip 2024-12-11 11:07:12 -05:00
Danilo Leal
dfe455b054 zeta: Improve UI for feedback instructions (#21857)
If the instructions are added as the input placeholder, when in a
smaller window size (like the one from the screenshot), scrolling is
needed to see them all. So, thought of extracting it out of there. Also
thought it looked more refined this way!

<img width="800" alt="Screenshot 2024-12-11 at 11 48 17"
src="https://github.com/user-attachments/assets/46974b94-6365-4a59-bf71-a6c0863aac68"
/>

Release Notes:

- N/A
2024-12-11 12:07:41 -03:00
Danilo Leal
db7e38464a zeta: Show keybinding on rating buttons (#21853)
<img width="800" alt="Screenshot 2024-12-11 at 10 57 00"
src="https://github.com/user-attachments/assets/6055639c-5b38-444d-b76d-bf7584a82efc"
/>

Release Notes:

- N/A
2024-12-11 11:54:39 -03:00
Kyle Kelley
f8b6d71670 Optimize REPL kernel spec refresh (#21844)
Python kernelspec refresh now only performed on (known) python files. 

Release Notes:

- N/A
2024-12-11 06:20:44 -08:00
Thorsten Ball
ae351298b4 zeta: Fixes to completion-rating modal (#21852)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 15:00:27 +01:00
Thorsten Ball
664468d468 zeta: Invalidate completion in different buffers (#21850)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 14:37:53 +01:00
Piotr Osiewicz
714f183ede multi_buffer: optimize runnables layout (#21849)
Related to #21481 ; it fixes a bunch of hotspots I saw while looking at
the provided profiles. MultiBuffer still takes up 100% CPU on the
foreground thread for me - this time around it's on selection updates
(when dragging the selected text towards an edge of a screen).

Release Notes:

- N/A
2024-12-11 13:46:08 +01:00
Mikayla Maki
b36dcf3b92 Improve Zeta rating ergonomics (#21845)
This PR adds keyboard shortcuts to common interactions you might want to
do in the Zeta rating panel.

This PR also adds a way to fake inline completion requests, as well as
the test data used to create this PR, to make it easier to adjust the UI
in the future.

It also changes the status bar from the text "Zeta" to "ζ", because I
thought it looked cool.

Release Notes:

- N/A
2024-12-11 01:57:46 -08:00
Danilo Leal
63e1bf01a4 zeta: Improve reviewing UI (#21838)
Starting to fine-tune it.

| No edits scenario | Rated edits scenario |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-11 at 01 57 46"
src="https://github.com/user-attachments/assets/42926e84-7a7f-4692-af44-672b52d3d377">
| <img width="1577" alt="Screenshot 2024-12-11 at 01 58 37"
src="https://github.com/user-attachments/assets/ee8ab0ef-75af-424c-b916-9f1ce8b5264d">

Release Notes:

- N/A
2024-12-11 02:19:57 -03:00
Connor Tsui
62a6a755ec Add musl package for Arch Linux (#21830)
It seems like `musl` is required to build on Arch Linux, but it is not included in the dependencies list.
2024-12-10 21:05:53 -05:00
Ethan Budd
28faba12a2 Recognize .C and .H as supported cpp extensions (#21647)
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-10 19:55:21 -05:00
Marshall Bowers
c255e55599 assistant2: Sketch in sending file context to model (#21829)
This PR sketches in support for sending file context attached to a
message to the model.

Right now the context is just mocked.

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 41 PM"
src="https://github.com/user-attachments/assets/3ee4e86a-7893-42dc-98f9-982aa202d310">

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 53 PM"
src="https://github.com/user-attachments/assets/8a3c2dd7-a466-4dbf-83ec-1c7d969c1a4b">

Release Notes:

- N/A
2024-12-10 16:35:53 -05:00
Joseph T. Lyons
f80eb73213 Update event type to conform to standard (#21827)
Release Notes:

- N/A
2024-12-10 16:14:31 -05:00
strowk
faf79e52fe zed_extension_api: Add a short explanation of repo format (#21824)
Improved extension api documentation for latest_github_release function

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-10 15:04:47 -05:00
Michael Sloan
ab595b0d55 Resolve documentation for visible completions (#21705)
Release Notes:

- Improved LSP resolution of documentation for completions. It now
queries documentation for visible completions and avoids doing too many
redundant queries.

---

In #21286, documentation resolution was made more efficient by only
resolving the current completion. However, this meant that single line
documentation shown inline in the menu was missing until scrolled
to. This also meant that it would wait for navigation to resolve
completion docs, leading to lag for displaying documentation.

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.
2024-12-10 12:25:30 -07:00
Michael Sloan
ab1e9bf270 On windows, recreate renderer swap chain on restore from minimized (#21756)
Closes #21688

Release Notes:

- Windows: Fix freeze after window minimize and maximize
2024-12-10 11:59:44 -07:00
Minqi Pan
adc66473e7 gpui: Add cursor style methods of nesw nwse resize (#21801)
Release Notes:

- N/A

---

This change adds two new methods to the cursor_style_methods function in
the gpui_macros crate (according to the Tailwind CSS documentation
https://tailwindcss.com/docs/cursor):
1. `cursor_nesw_resize`: Sets the cursor style to nesw-resize when
hovering over an element. This is useful for indicating resizing
diagonally from top-right to bottom-left.
2. `cursor_nwse_resize`: Sets the cursor style to nwse-resize when
hovering over an element. This is used for resizing diagonally from
top-left to bottom-right.
2024-12-10 11:54:26 -07:00
Marshall Bowers
119b5de384 assistant2: Change chat keybinding to just Enter (#21819)
This PR changes the Assistant2 chat keybinding from `Cmd-Enter` to just
`Enter`.

Release Notes:

- N/A
2024-12-10 12:34:54 -05:00
Marshall Bowers
c80ea60860 assistant2: Update to match latest designs (#21818)
This PR updates the Assistant2 panel to match the latest designs.

<img width="1159" alt="Screenshot 2024-12-10 at 11 49 14 AM"
src="https://github.com/user-attachments/assets/53739709-e7b9-4e35-8a5d-97b6560623ed">

Release Notes:

- N/A
2024-12-10 12:05:30 -05:00
Peter Tripp
bac6896786 Add Dart docs for line length (#21815) 2024-12-10 11:22:17 -05:00
Bennet Bo Fenner
c6932d1f51 zeta: Add action to clear edit history (#21813)
Co-Authored-by: Antonio <antonio@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-10 16:57:24 +01:00
Conrad Irwin
03efd0d1d9 Stop sending data to Clickhouse (#21763)
Release Notes:

- N/A
2024-12-10 08:47:29 -07:00
Thorsten Ball
43ba0c9fa6 zeta: Extend text in popover until EOL (#21811)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:21:45 +01:00
Thorsten Ball
4300ef840b zeta: Use word-wise diff when computing edits (#21810)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:05:34 +01:00
Bennet Bo Fenner
e0f4c01794 Revert "Improve project_panel diagnostic icon knockout colors (#20760)" (#21807)
This reverts commit 571c7d4f66.

Manually tracking the hovered entities causes issues with hightlighting:


https://github.com/user-attachments/assets/932dc022-a0ad-485c-a9db-ef03d7b86032

cc @danilo-leal @nilskch 

Release Notes:

- Fixed an issue where hovering over project panel would not update the
background correctly
2024-12-10 15:37:33 +01:00
Bennet Bo Fenner
58f9301253 image viewer: Allow dropping images on pane (#21803)
Partially addresses #21484


https://github.com/user-attachments/assets/777da5de-15c3-4af3-a597-1835c0155326

Release Notes:

- Support opening images by dropping them onto a pane
2024-12-10 15:01:14 +01:00
Thorsten Ball
96499b7b25 zeta: Refresh LLM token in case it expired (#21796)
Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 14:12:49 +01:00
Finn Evers
09006aaee9 Add option to activate left neighbour tab on tab close (#21800)
Closes #21738

Release Notes:

- Added `left_neighbour` option to the `tabs.activate_on_close` setting
to activate the left adjacent tab on tab close.
2024-12-10 15:05:36 +02:00
Kirill Bulatov
2ca3b440a9 Fix a panic when drop-splitting the terminal panel (#21795)
Closes https://github.com/zed-industries/zed/issues/21792

Release Notes:

- (Preview only) Fixed a panic when drop-splitting the terminal panel
2024-12-10 13:50:19 +02:00
Piotr Osiewicz
9219b05c85 chore: Move more local code into LocalLspStore (#21794)
Closes #ISSUE

Release Notes:

- N/A
2024-12-10 12:48:44 +01:00
Nils Koch
bd2087675b Fix git colors in image tabs (#21773)
Note that the git coloring of the icons got removed in
https://github.com/zed-industries/zed/pull/21383

Closes #21772

Release Notes:

- N/A
2024-12-10 01:40:25 -07:00
Jason Lee
44164dbbb8 gpui: Update Bounds, Point, and Axis to be serializable (#21783)
Makes `Bounds`, `Point`, and `Axis` be serializable, for dumping to JSON without conversion.

Release Notes:

- N/A
2024-12-10 00:43:55 -07:00
Conrad Irwin
3c053c7bc4 LspStore: move language_server_ids to local state too (#21786)
Attempt to further clarify what state is present in which mode

Release Notes:

- N/A
2024-12-10 00:15:06 -07:00
Conrad Irwin
48eed7499f Move diagnostics to the LocalLspStore (#21782)
This should be a no-op, but clarifies that some fields of the LspStore
were never actually used in the remote case.

Release Notes:

- N/A
2024-12-09 22:47:13 -07:00
Conrad Irwin
a35ef5b79f Fix diagnostics randomized tests (#21775)
These were silently passing after the delay in updating diagnostics was
added.

Co-Authored-By: Max <max@zed.dev>

cc @someonetoignore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-12-09 21:56:43 -07:00
Marshall Bowers
8a85d6ef96 collab: Make metrics_id required in LlmTokenClaims (#21771)
This PR makes the `metrics_id` field on the `LlmTokenClaims` required,
as we always have one in practice.

Release Notes:

- N/A
2024-12-09 17:58:14 -05:00
Marshall Bowers
158cdc33ba collab: Attach additional properties to Language Model Used event (#21770)
This PR attaches two new properties to the `Language Model Used` event:

- `has_llm_subscription` - This will tell us if a user is a paid
subscriber.
- `max_monthly_spend_in_cents` - This will indicate what their maximum
monthly spend is set to.

Release Notes:

- N/A
2024-12-09 17:13:41 -05:00
Marshall Bowers
bdeac79d48 collab: Prevent max_monthly_llm_usage_spending_in_cents from being negative (#21768)
This PR fixes an issue where the
`max_monthly_llm_usage_spending_in_cents` preference could be set to a
negative value.

Release Notes:

- N/A
2024-12-09 16:55:26 -05:00
Mikayla Maki
73e0d816c4 Move ContextMenu out of editor.rs and rename ContextMenu to CodeContextMenu (#21766)
This is a no-functionality refactor of where the `ContextMenu` type is
defined. Just the type definition and implementation is up to almost
1,000 lines; so I've moved it to it's own file and renamed the type to
`CodeContextMenu`

Release Notes:

- N/A
2024-12-09 13:31:20 -08:00
Conrad Irwin
6538227f07 Revert "Avoid endless loop of the diagnostic updates (#21209)" (#21764)
This reverts commit 9999c31859.

Release Notes:

- Fixes diagnostics not updating in some circumstances
2024-12-09 14:15:23 -07:00
Marshall Bowers
ef45eca88e extension_host: Fix uploading dev extensions to the remote server (#21761)
This PR fixes an issue where dev extensions were not working when
uploaded to the remote server.

The `extension.toml` for dev extensions may not contain all of the
information (such as the list of languages), as this is something that
we derive from the filesystem at packaging time. This meant that
uploading a dev extension that contained languages could have them
absent from the uploaded `extension.toml`.

For dev extensions we now upload a serialized version of the in-memory
extension manifest, which should have all of the information present.

Release Notes:

- SSH Remoting: Fixed an issue where some dev extensions would not work
after being uploaded to the remote server.

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-09 15:23:28 -05:00
Michael Sloan
803855e7b1 Add async_task::spawn_local variant that includes caller in panics (#21758)
For debugging #21020. Copy-modified [from async_task
here](ca9dbe1db9/src/runnable.rs (L432))

Release Notes:

- N/A
2024-12-09 12:45:37 -07:00
Kirill Bulatov
25a5ad54ae Sync newly added diff hunks (#21759)
Fixed project diff multi buffer not expanding its diff until edited

Release Notes:

- N/A
2024-12-09 21:43:25 +02:00
Michael Sloan
a5355e92e3 Add per-language settings show_completions_on_input and show_completion_documentation (#21722)
Release Notes:

- Added `show_completions_on_input` and `show_completion_documentation`
per-language settings. These settings were available before, but were
not configurable per-language.
2024-12-09 11:53:50 -07:00
Piotr Osiewicz
b7edf31170 lsp: Disable usage of follow-up completion invokes (#21755)
Some of our users ran into a peculiar bug: autoimports with vtsls were
leaving behind an extra curly brace. I think we were slightly incorrect
in always requesting a follow-up completion without regard for last
result of completion request (whether it was incomplete or not).
Specifically, we're falling into this branch in current form:
037c2b615b/packages/service/src/service/completion.ts (L121)
which then leads to incorrect edits being returned from vtsls.

Release Notes:

- Fixed an edge case with appliance of autocompletions in VTSLS that
could result in incorrect edits being applied.
2024-12-09 19:10:34 +01:00
Michael Sloan
7bd69130f8 Make space for documentation aside during followup completion select (#21716)
The goal of #7115 appears to be to limit the disruptiveness of
completion documentation load causing the completion selector to move
around. The approach was to debounce load of documentation via a setting
`completion_documentation_secondary_query_debounce`. This particularly
had a nonideal interaction with #21286, where now this debounce interval
was used between the documentation fetches of every individual
completion item.

I think a better solution is to continue making space for documentation
to be shown as soon as any documentation is shown. #21704 implemented
part of this, but it did not persist across followup completions.

Release Notes:

- Fixed completion list moving around on load of documentation. The
previous approach to mitigating this was to rate-limit the fetch of
docs, configured by a
`completion_documentation_secondary_query_debounce` setting, which is
now deprecated.
2024-12-09 10:47:14 -07:00
Alexandre Hamez
2af9fa7785 docs: Add missing ':' (#21751)
Release Notes:

- N/A
2024-12-09 12:22:19 -05:00
Michael Sloan
16ecbafa7a Skip spawning task for background_executor.timer(Duration::ZERO) (#21729)
Release Notes:

- N/A
2024-12-09 10:18:18 -07:00
Travis Stevens
e5f3a683f0 Fixing Missing comma (#21749)
Fix a missing comma in the docs

Release Notes:

- N/A
2024-12-09 18:49:40 +02:00
Marshall Bowers
8c91eecb67 call: Add test-support feature for livekit_client_macos (#21748)
This PR updates the `call` crate to include the `test-support` feature
for `livekit_client_macos` when `call` is used with `test-support`.

This fixes running `cargo test -p copilot` and `cargo test -p editor`
(and perhaps some other crates).

Release Notes:

- N/A
2024-12-09 11:21:02 -05:00
Thorsten Ball
8fcaf8b870 collab: Fix compilation error by removing dependency on livekit_client (#21744)
This fixes collab not being able to compile anymore for Linux:


https://github.com/zed-industries/zed/actions/runs/12236650046/job/34130962682

Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-09 15:14:46 +01:00
Antonio Scandurra
77b8296fbb Introduce staff-only inline completion provider (#21739)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Thorsten <thorsten@zed.dev>
2024-12-09 14:26:36 +01:00
Piotr Osiewicz
39e8944dcc language_tools: Split LSP log view selector into two (#21742)
This should make it easier to interact with LSP log view when there are
multiple language servers. I often find the current UI clunky when I
have over 5 servers running (which isn't uncommon with multiple projects
open)


https://github.com/user-attachments/assets/2ecaf17f-4b40-4c8f-aa6f-03b437a3d979


Closes #ISSUE

Release Notes:

- N/A
2024-12-09 14:10:11 +01:00
Danilo Leal
a7d12eea39 Enhance the Vim Mode toggle discoverability (#21589)
Closes https://github.com/zed-industries/zed/issues/21522

This PR adds an info tooltip on the Welcome screen, informing users how
Vim Mode can be toggled on and off. It also adds the Vim Mode toggle in
the Editor Controls menu. This is all so that folks who accidentally
turn it on better know how to turn it off. We're of course already able
to toggle this setting via the command palette, but that may be harder
to reach for beginners. So, maybe that's enough to close the linked
issue? Open to feedback.

(Note: I also added a max-width to the tooltip's label in this PR. I'm
confident that this won't make any tooltip look weird/broken, but if it
does, it may be because of this new property).

| Welcome Page | Editor Controls |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 11 20 04"
src="https://github.com/user-attachments/assets/1229f866-6be5-45cd-a6b8-c805f72144a6">
| <img width="800" alt="Screenshot 2024-12-05 at 11 12 15"
src="https://github.com/user-attachments/assets/f082d7f9-7d56-41d1-bc86-c333ad6264c7">
|

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-12-09 09:28:40 -03:00
Nils Koch
ce9e4629be Add go version to gopls cache key (#20922)
Closes #8071

Release Notes:

- Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version.

Previously we cached gopls (the go language server) as a file called
`gopls_{GOPLS_VERSION}`. The go version that gopls was built with is
crucial, so we need to cache the go version as well.

It's actually super interesting and very clever; gopls uses go to parse
the AST and do all the analyzation etc. Go exposes its internals in its
standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze
the user code. So if there is a new go release that contains new
syntax/features/etc. (the libraries `go/parser`, `go/types`, ...
change), we can rebuild the same version of `gopls` with the new version
of go (with the updated `go/xxx` libraries) to support the new language
features.

We had some issues around that (e.g., range over integers introduced in
go1.22, or custom iterators in go1.23) where we never updated gopls,
because we were on the latest gopls version, but built with an old go
version.

After this PR gopls will be cached under the name
`gopls_{GOPLS_VERSION}_go_{GO_VERSION}`.

Most users do not see this issue anymore, because after
https://github.com/zed-industries/zed/pull/8188 we first check if we can
find gopls in the PATH before downloading and caching gopls, but the
issue still exists.
2024-12-09 12:56:01 +01:00
Remco Smits
e58cdca044 Added JavaScript runnable detection for context and suite methods (#21719)
Fixes
https://github.com/zed-industries/zed/pull/21246#issuecomment-2525578141

<img width="545" alt="Screenshot 2024-12-08 at 22 58 33"
src="https://github.com/user-attachments/assets/2f303bfe-9718-4aa9-910e-613feca15ea8">
<img width="409" alt="Screenshot 2024-12-08 at 22 58 44"
src="https://github.com/user-attachments/assets/c4576cf7-fd71-44d2-911e-3ed944c9b794">

Release Notes:

- Added JavaScript runnable detection for `context` and `suite` methods
for mochajs framework
2024-12-09 13:17:51 +02:00
mgsloan@gmail.com
4564273322 Add comment explaining project panel behavior on right-click outside selection 2024-12-08 21:21:16 -07:00
João Marcos
55ee72d84a Simplify TextHighlights map (#21724)
Remove unnecessary `Option` wrapping.
2024-12-08 20:27:54 -07:00
tims
2ce01ead93 Fix right click selection behavior in project panel (#21707)
Closes #21605

Consider you have set of entries selected or even a single entry
selected, and you right click some other entry which is **not** part of
your selected set. This doesn't not clear existing entries selection
(which it should clear, as how file manager right-click logic works, see
more below).

This issue might lead unexpected operation like deletion applied on
those existing selected entries. This PR fixes it.

Release Notes:

- Fix right click selection behavior in project panel
2024-12-08 19:13:12 -07:00
Hendrik
bf1525588d Add .jj to default file exclusion (#21708)
Relates to #21538

Release Notes:

- Added `**/.jj` to the default file exclusion list.
2024-12-08 18:44:46 -07:00
Michael Sloan
d0e99f6496 Bump x11rb version to v0.13.1 (#21723)
From diff looks like no material differences. With a local checkout of
`v0.13.0` I get build errors due to warning checking when I use a `path
= ...` dependency, but it is fixed with `v0.13.1`.

I see mention of this in the [renovate configuration
PR](https://github.com/zed-industries/zed/pull/15132) but doesn't seem
like that initial batch of renovation happened.

Release Notes:

- N/A
2024-12-08 18:42:44 -07:00
Cole Miller
ac07b9197a gpui: Don't panic on failing to set X11 cursor style (#21689)
One more panic (well, two) that should be a `log_err`.

Release Notes:

- N/A
2024-12-08 13:30:23 -05:00
Michael Sloan
4b93a5ca44 Make completions selector continue to show docs aside if ever shown (#21704)
In #21286, documentation fetch was made more efficient by only
fetching the current completion. This has a side effect of causing the
aside to disappear and reappear when navigating the list. This is
particularly jarring when there isn't enough space for the aside,
causing the completions list to jump to the left.

The solution here is to continue to show the aside even if the current
selection does not yet have docs fetched.

Release Notes:

- N/A
2024-12-08 09:44:48 -07:00
Cole Miller
c5b6d78d5b project_diff: Keep going after failing to rescan a buffer (#21673)
I ran into a case locally where the project diff view was unexpectedly
empty because the first file to be scanned wasn't valid UTF-8, and the
inmost loop in `schedule_worktree_rescan` currently breaks when any
loading task fails. It seems like it might make more sense to continue
with the rest of the buffers in this case and also when
`Project::open_unstaged_changes` fails. I've left the error handling for
`update` as-is.

Release Notes:

- Fix project diff view missing files
2024-12-07 12:56:52 -05:00
Danilo Leal
eb3d3eaebf Adjust diagnostic in tabs behavior (#21671)
Follow up to https://github.com/zed-industries/zed/pull/21637

After discussing about this feature with the team, we've decided that
diagnostic display in tabs should be: 1) turned off by default, and 2)
only shown when there are file icons. The main reason here being to keep
Zed's UI uncluttered.

This means that you can technically have this setting:

```
  "tabs": {
    "show_diagnostics": "all"
  },
```

...and still don't see any diagnostics because you're missing
`file_icons": true`.

| Error with file icons | Error with no file icons |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-06 at 21 05 13"
src="https://github.com/user-attachments/assets/babf9cc3-b3b0-492e-9748-3e97d96ce90e">
| <img width="800" alt="Screenshot 2024-12-06 at 21 05 24"
src="https://github.com/user-attachments/assets/5247a5f1-55a0-4c56-8aaf-a0cdd115464f">
|


Release Notes:

- N/A
2024-12-07 11:00:31 -03:00
Piotr Osiewicz
fdc7751457 toolchains: Do not use as_json representation for PartialEq (#21682)
Closes #21679

Release Notes:

- N/A
2024-12-07 14:52:55 +01:00
Piotr Osiewicz
f561a91daf lsp: Add support for didRename/willRename LSP messages (#21651)
Closes #21564

Notably, RA will now rename module references if you change the source
file name via our project panel.

This PR is a tad bigger than necessary as I torn out the Model<> from
didSave watchers (I tried to reuse that code for the same purpose).
Release Notes:

- Added support for language server actions being executed on file
rename.
2024-12-07 13:08:18 +01:00
Kirill Bulatov
14ba4a9c94 Fix zoomed terminal pane issues on split (#21668)
Closes https://github.com/zed-industries/zed/issues/21652

* prevents zooming out the panel when any terminal pane is closed
* forces focus on new terminal panes, to prevent the workspace from
getting odd pane events in the background

Release Notes:

- (Preview only) Fixed zoomed terminal pane issues on split
2024-12-07 10:39:01 +02:00
Cole Miller
fa7dddd6b5 gpui: Don't panic when failing to exec system opener (#21674) 2024-12-06 22:11:40 -05:00
Conrad Irwin
4d22a07a1e Remove last few alt- bindings (#21669)
Although I hoped we could keep the non-ascii alt characters, it turns
out this is not the case for all keyboards.

Fixes #21175

Release Notes:

- (breaking change) editor::ShowInlineCompetion is now `option-tab` on
macOS
(not `option-/`). editor::{Next,Previous}Completion are `option-tab` and
  `option-shift-tab` (not `option-[` and `option-]`). This fixes typing
  characters generated by option-{/,[,]} on keyboards like Croatian.
2024-12-06 16:43:12 -07:00
Conrad Irwin
9e287b33e5 Update NorwegianExtended equivalents (#21665)
Release Notes:

- Impoved key equivalents for Norwegian Extended layout
2024-12-06 16:42:58 -07:00
Conrad Irwin
9d44ed0894 Stop overriding cancelOperation (#21667)
This was added before we were handling key equivalents, and is no longer
needed. Furthermore in the gpui2 re-write we stopped sending the correct
modifiers so this hasn't worked for the last year.

Fixes #21520

Release Notes:

- Fixed a bug where cmd-escape could act like .
2024-12-06 16:42:50 -07:00
Matin Aniss
21a6664cf8 gpui: Support animated WebP image (#20778)
Add support for decoding animated WebP images into their individual
frames.

Release Notes:

- N/A
2024-12-06 14:53:27 -08:00
Joseph T. Lyons
e019d1405a Send an event when user changes their max monthly spend limit (#21664)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-06 17:35:00 -05:00
feeiyu
e5374f5d7d windows: Ignore WM_SIZE event when minimizing window (#21533)
Closes #21364

Release Notes:

- Fixed minimize window and then reopen cause the layout changed


![layout1204](https://github.com/user-attachments/assets/e823da90-0cc6-4fc9-8b8e-82680357c6fe)
2024-12-06 14:15:04 -08:00
Mikayla Maki
de939e718a Simplify livekit config so that cargo check Just Works (#21661)
Supersedes https://github.com/zed-industries/zed/pull/21653

This enables us to use `cargo test -p workspace` on macOS and Linux.

Note that the line diffs in `shared_screen.rs` are spurious, I just
re-ordered the `macos` and `cross-platform` modules to match the order
in the call crate.

Release Notes:

- N/A
2024-12-06 13:50:59 -08:00
geemili
7d80d1208c vim: Add delete action to HelixNormal mode (#21544)
Related issue: https://github.com/zed-industries/zed/issues/4642

Release-Notes:

* N/A
2024-12-06 14:05:41 -07:00
Conrad Irwin
78ca297282 Make use_key_equivalents opt-in (#21662)
When revamping international keyboard shortcuts I wanted to make the
default to use key equivalents; in hindsight, this is not what people
expect.

Release Notes:

- (Breaking) In keymap.json `"use_layout_keys": true` is now the
default. If you want to opt-out of this behaviour, set
`"use_key_equivalents": true` to have keys mapped for your keyboard. See
[documentation](https://zed.dev/docs/key-bindings#non-qwerty-keyboards)

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-06 14:05:03 -07:00
The Bearodactyl
17448f23a6 docs: Add clarification in Windows build instructions (#21659) 2024-12-06 15:19:36 -05:00
Conrad Irwin
e730a9d029 Bump to livekit 1.1.6 (#21660)
Co-Authored-By: Mikayla <mikayla@zed.dev>

This bumps to the latest v1 version of swift SDK. We could bump to 2,
but it
sounds like this will already have some race condition fixes (and a
click
around locally seems less prone to deadlocking so far...)

Release Notes:

- N/A
2024-12-06 12:06:55 -08:00
Marshall Bowers
5142e38d2b editor: Add actions for inserting UUIDs (#21656)
This PR adds two new actions for generating and inserting UUIDs into the
buffer:


https://github.com/user-attachments/assets/a3445a98-07e2-40b8-9773-fd750706cbcc

Release Notes:

- Added `editor: insert uuid v4` and `editor: insert uuid v7` actions
for inserting generated UUIDs into the editor.
2024-12-06 14:32:09 -05:00
Peter Tripp
7a1a7929bd docs: Add x.ai Grok example (#21655)
- Closes https://github.com/zed-industries/zed/issues/21635

<img width="639" alt="Screenshot 2024-12-06 at 13 57 42"
src="https://github.com/user-attachments/assets/a4434edb-3c7d-40c0-9df8-7e928a9307d0">


Release Notes:

- Document support for x.ai Grok
2024-12-06 13:59:40 -05:00
renovate[bot]
0368fff030 Update cloudflare/wrangler-action digest to 6d58852 (#21551)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action)
| action | digest | `05f17c4` -> `6d58852` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS40Mi40IiwidXBkYXRlZEluVmVyIjoiMzkuNDIuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 13:16:53 -05:00
Jax Young
99c31816c9 docs: Correct default values (#20897)
Some default values in the doc are outdated.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-06 12:47:05 -05:00
uncenter
feb2d85a13 Add YAML/TOML frontmatter injections for markdown (#21503)
Closes #7938. Adds front-matter injections for TOML/YAML in markdown. 
- See: https://github.com/tree-sitter-grammars/tree-sitter-markdown/blob/split_parser/tree-sitter-markdown/queries/injections.scm.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-06 12:34:15 -05:00
renovate[bot]
d6e11c58db Update Rust crate pathdiff to v0.2.3 (#21568)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pathdiff](https://redirect.github.com/Manishearth/pathdiff) |
workspace.dependencies | patch | `0.2.2` -> `0.2.3` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS40Mi40IiwidXBkYXRlZEluVmVyIjoiMzkuNDIuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:33:33 -05:00
renovate[bot]
8a6c2bb749 Update Rust crate rsa to v0.9.7 (#21570)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rsa](https://redirect.github.com/RustCrypto/RSA) |
workspace.dependencies | patch | `0.9.6` -> `0.9.7` |

---

### Release Notes

<details>
<summary>RustCrypto/RSA (rsa)</summary>

###
[`v0.9.7`](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7)

[Compare
Source](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS40Mi40IiwidXBkYXRlZEluVmVyIjoiMzkuNDIuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:32:45 -05:00
Bennet Bo Fenner
b4f59284a9 markdown preview: Allow clicking on image to navigate to source location (#21630)
Follow up to #21082

Similar to checkboxes, you can now click on the image to navigate to the
source location, cmd-clicking opens the url in the browser.


https://github.com/user-attachments/assets/edaaa580-9d8f-490b-a4b3-d6ffb21f197c


Release Notes:

- N/A
2024-12-06 18:31:58 +01:00
tims
bffdc55d63 linux: Make prompt detail selectable (#21405)
Closes #21305

As Linux doesn’t have native prompts, Zed uses a custom GPU-based
prompt, like the "About Zed" prompt. Currently, the detail in the prompt
isn’t selectable.

This PR fixes that by using the editor's multi-line selectable
functionality to make the detail selectable (and thus copyable). It
achieves this by disabling editing and setting the cursor to
transparent. The editor also does all the heavy lifting, like
double-clicking to select a word or triple-clicking to select a line,
like what user expects from selectable.

Before/After:

<img
src="https://github.com/user-attachments/assets/2012a6cc-a1ed-4efe-8bfb-440a9259f07a"
alt="before" width="360px" />

<img
src="https://github.com/user-attachments/assets/31922ef5-cb2d-4e90-a1a1-00843e767432"
alt="after" width="360px" />

When detail is `None` or empty string:

<img
src="https://github.com/user-attachments/assets/2be5c921-bda1-4db3-85cd-b4b0e2df86d2"
alt="none" width="360px" />

Release Notes:

- N/A
2024-12-06 09:26:47 -08:00
renovate[bot]
9ca0d99cfd Update Rust crate ctor to v0.2.9 (#21561)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ctor](https://redirect.github.com/mmastrac/rust-ctor) |
workspace.dependencies | patch | `0.2.8` -> `0.2.9` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS40Mi40IiwidXBkYXRlZEluVmVyIjoiMzkuNDIuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:22:35 -05:00
tims
e5251f4091 Fix incorrect language selected in language selector (#21648)
Due to filtering after enumeration, initial candidate ids are assigned
incorrectly. This later causes the wrong item to be picked up when
accessed via index in the vector.
2024-12-06 12:03:58 -05:00
Cole Miller
304158ed79 Catch panic from oo7 when reading credentials (#21617) 2024-12-06 08:45:03 -05:00
Danilo Leal
e8f0ebc881 Refine diagnostic icons in tabs (#21637)
Follow up to https://github.com/zed-industries/zed/pull/21383

Mostly adjusting the alignment when there are no file icons.

<img width="800" alt="Screenshot 2024-12-06 at 08 35 48"
src="https://github.com/user-attachments/assets/6a4206cc-2af5-4317-a92e-49dffa37de99">

Release Notes:

- N/A
2024-12-06 09:17:48 -03:00
Danilo Leal
7b1d1bf79e Update panel.focused_border token across themes (#21612)
Follow up to https://github.com/zed-industries/zed/pull/21593

This PR updates all built-in themes `panel.focused_border` tokens using
the same HEX code used for `text_accent`.

There shouldn't be any visual change here given the project panel item,
when focused, was using `Color::Selected`, which maps to `text_accent`,
to color its border. In the linked PR above, the project panel item was
updated to use the dedicated token for that. This is good because now
theme markers will be able to customize them separately (e.g., having a
different `text_accent` color than `panel.focused_border`).

Release Notes:

- N/A
2024-12-06 09:17:34 -03:00
Nils Koch
4b16b73f80 Fix panel.background color override (#21559)
Closes #21266

Release Notes:

- Fixes not using the `panel.background` color in the file tree

See comments in https://github.com/zed-industries/zed/issues/21266 for
more details.

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-06 09:17:24 -03:00
Bennet Bo Fenner
7e40addb5f markdown preview: Fix panic when parsing empty image tag (#21616)
Closes #21534

While investigating the panic, I noticed that the code was pretty
complicated and decided to refactor parts of it to reduce redundancy.

Release Notes:

- Fixed an issue where the app could crash when opening the markdown
preview with a malformed image tag
2024-12-06 10:01:57 +01:00
Max Brunsfeld
f6b5e1734e Get unstaged changes when excerpts of new buffers are added (#21619)
This fixes an error on nightly, introduced in
https://github.com/zed-industries/zed/pull/21258, where diffs were not
shown for buffers that were added to multi-buffers after construction.

Release Notes:

- N/A
2024-12-05 16:52:14 -08:00
Mikayla Maki
cf4e847c62 Implement session-global include_warnings in the diagnostic item (#21618)
Release Notes:

- Make the include warnings toggle in the diagnostic tab global for a
zed session.
2024-12-05 16:32:17 -08:00
Nick Breaton
aff17322f3 Detect wider variety of usernames for SSH-based remotes (#21508)
Closes #21507

Release Notes:

- Fixed detection of git remotes when using SSH and username is not
"git".
2024-12-05 15:23:37 -08:00
renovate[bot]
28650b2fac Update Rust crate blake3 to v1.5.5 (#21554)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [blake3](https://redirect.github.com/BLAKE3-team/BLAKE3) |
workspace.dependencies | patch | `1.5.4` -> `1.5.5` |

---

### Release Notes

<details>
<summary>BLAKE3-team/BLAKE3 (blake3)</summary>

###
[`v1.5.5`](https://redirect.github.com/BLAKE3-team/BLAKE3/releases/tag/1.5.5)

[Compare
Source](https://redirect.github.com/BLAKE3-team/BLAKE3/compare/1.5.4...1.5.5)

version 1.5.5

Changes since 1.5.4:

-   `b3sum --check` now supports checkfiles with Windows-style newlines.
    `b3sum` still emits Unix-style newlines, even on Windows, but
    sometimes text editors or version control tools will swap them.
-   The "digest" feature (deleted in v1.5.2) has been added back to the
    `blake3` crate. This is for backwards compatibility only, and it's
    insta-deprecated. All callers should prefer the "traits-preview"
    feature.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS40Mi40IiwidXBkYXRlZEluVmVyIjoiMzkuNDIuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 15:23:08 -08:00
203 changed files with 11791 additions and 6479 deletions

View File

@@ -129,8 +129,9 @@ jobs:
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
cargo check -p workspace --features "livekit-cross-platform"
cargo check -p workspace
cargo build -p remote_server
script/check-rust-livekit-macos
linux_tests:
timeout-minutes: 60
@@ -162,8 +163,10 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
- name: Build other binaries and features
run: |
cargo build -p zed
cargo check -p workspace
build_remote_server:
timeout-minutes: 60

View File

@@ -37,28 +37,28 @@ jobs:
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

509
Cargo.lock generated
View File

@@ -184,16 +184,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "annotate-snippets"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
dependencies = [
"unicode-width",
"yansi-term",
]
[[package]]
name = "anstream"
version = "0.6.18"
@@ -1691,27 +1681,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"annotate-snippets",
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.87",
]
[[package]]
name = "bindgen"
version = "0.70.1"
@@ -1869,9 +1838,9 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.5.4"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e"
dependencies = [
"arrayref",
"arrayvec",
@@ -2506,49 +2475,6 @@ dependencies = [
"util",
]
[[package]]
name = "clickhouse"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f"
dependencies = [
"bstr",
"bytes 1.8.0",
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.31",
"hyper 0.14.31",
"hyper-tls",
"lz4",
"sealed",
"serde",
"static_assertions",
"thiserror 1.0.69",
"tokio",
"url",
]
[[package]]
name = "clickhouse-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18af5425854858c507eec70f7deb4d5d8cec4216fcb086283a78872387281ea5"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.26.0",
"syn 1.0.109",
]
[[package]]
name = "clickhouse-rs-cityhash-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4baf9d4700a28d6cb600e17ed6ae2b43298a5245f1f76b4eab63027ebfd592b9"
dependencies = [
"cc",
]
[[package]]
name = "client"
version = "0.1.0"
@@ -2699,7 +2625,6 @@ dependencies = [
"call",
"channel",
"chrono",
"clickhouse",
"client",
"clock",
"collab_ui",
@@ -3007,15 +2932,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie-factory"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
dependencies = [
"futures 0.3.31",
]
[[package]]
name = "copilot"
version = "0.1.0"
@@ -3109,19 +3025,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-graphics-helmer-fork"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
@@ -3193,7 +3096,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b"
dependencies = [
"bindgen 0.70.1",
"bindgen",
]
[[package]]
@@ -3547,7 +3450,7 @@ version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [
"nix 0.29.0",
"nix",
"windows-sys 0.59.0",
]
@@ -3849,12 +3752,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -4016,6 +3913,7 @@ dependencies = [
"unindent",
"url",
"util",
"uuid",
"workspace",
]
@@ -4576,7 +4474,7 @@ dependencies = [
"serde_derive",
"serde_json",
"smol",
"sysinfo 0.31.4",
"sysinfo",
"ui",
"urlencoding",
"util",
@@ -5190,6 +5088,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
"regex",
"rope",
"serde",
"serde_json",
@@ -5234,6 +5133,25 @@ dependencies = [
"util",
]
[[package]]
name = "git_ui"
version = "0.1.0"
dependencies = [
"editor",
"git",
"gpui",
"itertools 0.13.0",
"menu",
"project",
"serde",
"settings",
"smallvec",
"theme",
"ui",
"windows 0.58.0",
"workspace",
]
[[package]]
name = "glob"
version = "0.3.1"
@@ -5354,7 +5272,7 @@ dependencies = [
"ashpd",
"async-task",
"backtrace",
"bindgen 0.70.1",
"bindgen",
"blade-graphics",
"blade-macros",
"blade-util",
@@ -5401,7 +5319,6 @@ dependencies = [
"raw-window-handle",
"refineable",
"resvg",
"scap",
"schemars",
"seahash",
"semantic_version",
@@ -5966,7 +5883,7 @@ dependencies = [
"http 1.1.0",
"hyper 1.5.0",
"hyper-util",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-native-certs 0.8.0",
"rustls-pki-types",
"tokio",
@@ -6229,6 +6146,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"db",
"editor",
"file_icons",
"gpui",
"project",
@@ -6329,8 +6247,6 @@ version = "0.1.0"
dependencies = [
"gpui",
"language",
"project",
"text",
]
[[package]]
@@ -6340,6 +6256,7 @@ dependencies = [
"anyhow",
"copilot",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
@@ -6355,6 +6272,7 @@ dependencies = [
"ui",
"workspace",
"zed_actions",
"zeta",
]
[[package]]
@@ -6977,12 +6895,6 @@ dependencies = [
"spin",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leb128"
version = "0.2.5"
@@ -6997,9 +6909,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.164"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "libdbus-sys"
@@ -7070,34 +6982,6 @@ dependencies = [
"redox_syscall 0.5.7",
]
[[package]]
name = "libspa"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810"
dependencies = [
"bitflags 2.6.0",
"cc",
"convert_case 0.6.0",
"cookie-factory",
"libc",
"libspa-sys",
"nix 0.27.1",
"nom",
"system-deps",
]
[[package]]
name = "libspa-sys"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f"
dependencies = [
"bindgen 0.69.5",
"cc",
"system-deps",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
@@ -7297,7 +7181,6 @@ dependencies = [
"nanoid",
"parking_lot",
"postage",
"scap",
"serde",
"serde_json",
"sha2",
@@ -7428,25 +7311,6 @@ dependencies = [
"url",
]
[[package]]
name = "lz4"
version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d1febb2b4a79ddd1980eede06a8f7902197960aa0383ffcfdd62fe723036725"
dependencies = [
"lz4-sys",
]
[[package]]
name = "lz4-sys"
version = "1.11.1+lz4-1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -7515,6 +7379,7 @@ dependencies = [
"settings",
"theme",
"ui",
"util",
"workspace",
]
@@ -7626,7 +7491,7 @@ name = "media"
version = "0.1.0"
dependencies = [
"anyhow",
"bindgen 0.70.1",
"bindgen",
"core-foundation 0.9.4",
"ctor",
"foreign-types 0.5.0",
@@ -7917,17 +7782,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -8254,36 +8108,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
"objc_exception",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_exception"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
dependencies = [
"cc",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
@@ -8712,9 +8536,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pathfinder_geometry"
@@ -9386,34 +9210,6 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pipewire"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda"
dependencies = [
"anyhow",
"bitflags 2.6.0",
"libc",
"libspa",
"libspa-sys",
"nix 0.27.1",
"once_cell",
"pipewire-sys",
"thiserror 1.0.69",
]
[[package]]
name = "pipewire-sys"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112"
dependencies = [
"bindgen 0.69.5",
"libspa-sys",
"system-deps",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@@ -10067,7 +9863,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.16",
"rustls 0.23.18",
"socket2 0.5.7",
"thiserror 2.0.3",
"tokio",
@@ -10085,7 +9881,7 @@ dependencies = [
"rand 0.8.5",
"ring",
"rustc-hash 2.0.0",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-pki-types",
"slab",
"thiserror 2.0.3",
@@ -10510,7 +10306,7 @@ dependencies = [
"settings",
"shellexpand 2.1.2",
"smol",
"sysinfo 0.31.4",
"sysinfo",
"telemetry_events",
"toml 0.8.19",
"util",
@@ -10644,7 +10440,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-native-certs 0.8.0",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@@ -10856,9 +10652,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
dependencies = [
"const-oid",
"digest",
@@ -11032,9 +10828,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.16"
version = "0.23.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
dependencies = [
"once_cell",
"ring",
@@ -11164,26 +10960,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scap"
version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14ca85dd7aa5f0e11e093c1c32b306666a52903db1bd89aa6442967a03cd8bd2"
dependencies = [
"cocoa 0.25.0",
"core-graphics-helmer-fork",
"dbus",
"objc",
"pipewire",
"rand 0.8.5",
"screencapturekit",
"screencapturekit-sys",
"sysinfo 0.30.13",
"tao-core-video-sys",
"windows 0.58.0",
"windows-capture",
]
[[package]]
name = "schannel"
version = "0.1.26"
@@ -11214,7 +10990,7 @@ checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.29.1",
"serde_derive_internals",
"syn 2.0.87",
]
@@ -11236,29 +11012,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "screencapturekit"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e"
dependencies = [
"screencapturekit-sys",
]
[[package]]
name = "screencapturekit-sys"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60"
dependencies = [
"block",
"dispatch",
"objc",
"objc-foundation",
"objc_id",
"once_cell",
]
[[package]]
name = "scrypt"
version = "0.11.0"
@@ -11374,18 +11127,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sealed"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c"
dependencies = [
"heck 0.3.3",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "search"
version = "0.1.0"
@@ -11533,17 +11274,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "serde_derive_internals"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
@@ -11566,9 +11296,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.6.0",
"itoa",
@@ -12122,9 +11852,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -12135,9 +11865,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08"
dependencies = [
"atoi",
"bigdecimal",
@@ -12163,7 +11893,7 @@ dependencies = [
"paste",
"percent-encoding",
"rust_decimal",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
@@ -12182,9 +11912,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc"
dependencies = [
"proc-macro2",
"quote",
@@ -12195,9 +11925,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce"
dependencies = [
"dotenvy",
"either",
@@ -12221,9 +11951,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -12268,9 +11998,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -12312,9 +12042,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e"
dependencies = [
"atoi",
"chrono",
@@ -12697,21 +12427,6 @@ dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"ntapi",
"once_cell",
"rayon",
"windows 0.52.0",
]
[[package]]
name = "sysinfo"
version = "0.31.4"
@@ -12839,18 +12554,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
[[package]]
name = "tao-core-video-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"objc",
]
[[package]]
name = "tap"
version = "1.0.1"
@@ -12967,7 +12670,7 @@ dependencies = [
"serde_derive",
"settings",
"smol",
"sysinfo 0.31.4",
"sysinfo",
"task",
"theme",
"thiserror 1.0.69",
@@ -13415,7 +13118,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-pki-types",
"tokio",
]
@@ -14246,6 +13949,7 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"log",
"rand 0.8.5",
"regex",
@@ -15275,16 +14979,6 @@ dependencies = [
"wasmtime-environ",
]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.54.0"
@@ -15315,20 +15009,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-capture"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6001b777f61cafce437201de46a019ed7f4afed3b669f02e5ce4e0759164cb3e"
dependencies = [
"clap",
"ctrlc",
"parking_lot",
"rayon",
"thiserror 1.0.69",
"windows 0.58.0",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -16115,15 +15795,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yansi-term"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
dependencies = [
"winapi",
]
[[package]]
name = "yazi"
version = "0.1.6"
@@ -16187,7 +15858,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix 0.29.0",
"nix",
"ordered-stream",
"rand 0.8.5",
"serde",
@@ -16224,7 +15895,7 @@ dependencies = [
"futures-core",
"futures-util",
"hex",
"nix 0.29.0",
"nix",
"ordered-stream",
"serde",
"serde_repr",
@@ -16333,6 +16004,7 @@ dependencies = [
"futures 0.3.31",
"git",
"git_hosting_providers",
"git_ui",
"go_to_line",
"gpui",
"http_client",
@@ -16349,10 +16021,11 @@ dependencies = [
"languages",
"libc",
"log",
"markdown",
"markdown_preview",
"menu",
"mimalloc",
"nix 0.29.0",
"nix",
"node_runtime",
"notifications",
"outline",
@@ -16383,7 +16056,7 @@ dependencies = [
"snippet_provider",
"snippets_ui",
"supermaven",
"sysinfo 0.31.4",
"sysinfo",
"tab_switcher",
"task",
"tasks_ui",
@@ -16409,6 +16082,7 @@ dependencies = [
"winresource",
"workspace",
"zed_actions",
"zeta",
]
[[package]]
@@ -16719,6 +16393,43 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "zeta"
version = "0.1.0"
dependencies = [
"anyhow",
"call",
"client",
"clock",
"collections",
"ctor",
"editor",
"env_logger 0.11.5",
"futures 0.3.31",
"gpui",
"http_client",
"indoc",
"inline_completion",
"language",
"language_models",
"log",
"menu",
"reqwest_client",
"rpc",
"serde_json",
"settings",
"similar",
"telemetry_events",
"theme",
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"util",
"uuid",
"workspace",
"worktree",
]
[[package]]
name = "zip"
version = "0.6.6"

View File

@@ -141,6 +141,8 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -226,6 +228,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
@@ -325,6 +328,7 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
#
# External crates
@@ -358,7 +362,6 @@ cargo_metadata = "0.19"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"
@@ -441,7 +444,6 @@ rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustls = "0.21.12"
rustls-native-certs = "0.8.0"
scap = "0.0.7"
schemars = { version = "0.8", features = ["impl_json_schema"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -507,7 +509,7 @@ unindent = "0.1.7"
unicode-segmentation = "1.10"
unicode-script = "0.5.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
wasmparser = "0.215"
wasm-encoder = "0.215"
wasmtime = { version = "24", default-features = false, features = [

4
assets/icons/eraser.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
<path d="M22 21H7"/><path d="m5 11 9 9"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

After

Width:  |  Height:  |  Size: 348 B

12
assets/icons/info.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4L4 12H12L8 4Z" fill="currentColor"/>
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L3 12H14L8.5 3Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 150 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4.5L12 11.5M12 4.5L5 11.5" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 199 B

View File

@@ -1,6 +1,7 @@
[
// Standard macOS bindings
{
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
@@ -40,6 +41,7 @@
},
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
@@ -64,6 +66,7 @@
"cmd-v": "editor::Paste",
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp",
@@ -131,6 +134,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -148,20 +152,23 @@
},
{
"context": "Editor && mode == full && inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"alt-tab": "editor::NextInlineCompletion",
"alt-shift-tab": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
}
},
{
"context": "Editor && !inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-\\": "editor::ShowInlineCompletion"
"alt-tab": "editor::ShowInlineCompletion"
}
},
{
"context": "Editor && mode == auto_height",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -170,12 +177,14 @@
},
{
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
@@ -183,6 +192,7 @@
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-g": "search::SelectNextMatch",
@@ -195,6 +205,7 @@
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
@@ -209,6 +220,7 @@
},
{
"context": "AssistantPanel2",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
@@ -216,12 +228,14 @@
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant2::Chat"
"enter": "assistant2::Chat"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
@@ -230,6 +244,7 @@
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -243,6 +258,7 @@
},
{
"context": "BufferSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -250,6 +266,7 @@
},
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -257,6 +274,7 @@
},
{
"context": "ProjectSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -268,6 +286,7 @@
},
{
"context": "ProjectSearchBar > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -275,6 +294,7 @@
},
{
"context": "ProjectSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -282,6 +302,7 @@
},
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -292,6 +313,7 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-}": "pane::ActivateNextItem",
@@ -320,6 +342,7 @@
// Bindings from VS Code
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-[": "editor::Outdent",
"cmd-]": "editor::Indent",
@@ -383,6 +406,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
@@ -390,6 +414,7 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": ["pane::ActivateItem", 0],
"ctrl-2": ["pane::ActivateItem", 1],
@@ -409,6 +434,7 @@
},
{
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
@@ -464,6 +490,7 @@
},
{
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
@@ -474,6 +501,7 @@
// Bindings from Sublime Text
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -493,6 +521,7 @@
// Bindings from Atom
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-k up": "pane::SplitUp",
"cmd-k down": "pane::SplitDown",
@@ -503,12 +532,14 @@
// Bindings that should be unified with bindings for more general actions
{
"context": "Editor && renaming",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmRename"
}
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
@@ -516,18 +547,21 @@
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCodeAction"
}
},
{
"context": "Editor && (showing_code_actions || showing_completions)",
"use_key_equivalents": true,
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
@@ -539,6 +573,7 @@
},
// Custom bindings
{
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
@@ -549,6 +584,7 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
@@ -560,6 +596,7 @@
},
{
"context": "ProposedChangesEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
@@ -567,6 +604,7 @@
},
{
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
@@ -574,12 +612,14 @@
},
{
"context": "ProjectSearchBar && !in_replace",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
@@ -596,6 +636,7 @@
},
{
"context": "ProjectPanel",
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
@@ -625,12 +666,14 @@
},
{
"context": "ProjectPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"space": "project_panel::Open"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
@@ -638,18 +681,21 @@
},
{
"context": "(CollabPanel && editing) > Editor",
"use_key_equivalents": true,
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "ChannelModal",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "picker::ConfirmCompletion",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
@@ -658,18 +704,21 @@
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"use_key_equivalents": true,
"bindings": {
"cmd": "file_finder::ToggleMenu"
}
},
{
"context": "FileFinder && !menu_open",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd-j": "pane::SplitDown",
@@ -680,6 +729,7 @@
},
{
"context": "FileFinder && menu_open",
"use_key_equivalents": true,
"bindings": {
"j": "pane::SplitDown",
"k": "pane::SplitUp",
@@ -689,6 +739,7 @@
},
{
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
@@ -698,6 +749,7 @@
},
{
"context": "Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy",
@@ -737,5 +789,24 @@
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
}
]

View File

@@ -1,7 +1,6 @@
[
{
"context": "VimControl && !menu",
"use_layout_keys": true,
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -188,7 +187,6 @@
},
{
"context": "vim_mode == normal",
"use_layout_keys": true,
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
@@ -243,7 +241,6 @@
},
{
"context": "VimControl && VimCount",
"use_layout_keys": true,
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
@@ -251,7 +248,6 @@
},
{
"context": "vim_mode == visual",
"use_layout_keys": true,
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
@@ -301,7 +297,6 @@
},
{
"context": "vim_mode == insert",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -331,6 +326,7 @@
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"d": "vim::HelixDelete",
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
@@ -344,7 +340,6 @@
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
"bindings": {
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
@@ -352,7 +347,6 @@
},
{
"context": "vim_mode == replace",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -370,7 +364,6 @@
},
{
"context": "vim_mode == waiting",
"use_layout_keys": true,
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
@@ -384,7 +377,6 @@
},
{
"context": "vim_mode == operator",
"use_layout_keys": true,
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
@@ -394,7 +386,6 @@
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"use_layout_keys": true,
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
@@ -425,7 +416,6 @@
},
{
"context": "vim_operator == c",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
@@ -434,7 +424,6 @@
},
{
"context": "vim_operator == d",
"use_layout_keys": true,
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
@@ -444,7 +433,6 @@
},
{
"context": "vim_operator == gu",
"use_layout_keys": true,
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
@@ -452,7 +440,6 @@
},
{
"context": "vim_operator == gU",
"use_layout_keys": true,
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
@@ -460,7 +447,6 @@
},
{
"context": "vim_operator == g~",
"use_layout_keys": true,
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
@@ -468,7 +454,6 @@
},
{
"context": "vim_operator == gq",
"use_layout_keys": true,
"bindings": {
"g q": "vim::CurrentLine",
"q": "vim::CurrentLine",
@@ -478,7 +463,6 @@
},
{
"context": "vim_operator == y",
"use_layout_keys": true,
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
@@ -486,42 +470,36 @@
},
{
"context": "vim_operator == ys",
"use_layout_keys": true,
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"use_layout_keys": true,
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"use_layout_keys": true,
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"use_layout_keys": true,
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "vim_mode == literal",
"use_layout_keys": true,
"bindings": {
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
@@ -565,7 +543,6 @@
},
{
"context": "BufferSearchBar && !in_replace",
"use_layout_keys": true,
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
@@ -573,7 +550,6 @@
},
{
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -630,7 +606,6 @@
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"g /": "pane::DeploySearch"
@@ -639,7 +614,6 @@
{
// netrw compatibility
"context": "ProjectPanel && not_editing",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"%": "project_panel::NewFile",
@@ -673,7 +647,6 @@
},
{
"context": "OutlinePanel && not_editing",
"use_layout_keys": true,
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",

View File

@@ -150,9 +150,6 @@
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
@@ -564,12 +561,15 @@
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
// "history"
// 2. Activate the right neighbour tab if present
// "neighbour"
// 3. Activate the left neighbour tab if present
// "left_neighbour"
"activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// This setting can take the following three values:
/// Diagnostics are only shown when file icons are also active.
/// This setting only works when can take the following three values:
///
/// 1. Do not mark any files:
/// "off"
@@ -577,7 +577,7 @@
/// "errors"
/// 3. Mark files with errors and warnings:
/// "all"
"show_diagnostics": "all"
"show_diagnostics": "off"
},
// Settings related to preview tabs.
"preview_tabs": {
@@ -684,6 +684,7 @@
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#1e2025ff",
"search.match_background": "#11a79366",
"panel.background": "#21242bff",
"panel.focused_border": null,
"panel.focused_border": "#10a793ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#f7f7f84c",
"scrollbar.thumb.hover_background": "#252931ff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#19171cff",
"search.match_background": "#576dda66",
"panel.background": "#221f26ff",
"panel.focused_border": null,
"panel.focused_border": "#566ddaff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#efecf44c",
"scrollbar.thumb.hover_background": "#332f38ff",
@@ -431,7 +431,7 @@
"tab.active_background": "#efecf4ff",
"search.match_background": "#586dda66",
"panel.background": "#e6e3ebff",
"panel.focused_border": null,
"panel.focused_border": "#586cdaff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#19171c4c",
"scrollbar.thumb.hover_background": "#cbc8d1ff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"panel.background": "#1f2127ff",
"panel.focused_border": null,
"panel.focused_border": "#5ac1feff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#bfbdb64c",
"scrollbar.thumb.hover_background": "#2d2f34ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"panel.background": "#ececedff",
"panel.focused_border": null,
"panel.focused_border": "#3b9ee5ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#5c61664c",
"scrollbar.thumb.hover_background": "#dfe0e1ff",

View File

@@ -55,7 +55,7 @@
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"panel.background": "#3a3735ff",
"panel.focused_border": null,
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
@@ -439,7 +439,7 @@
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"panel.background": "#393634ff",
"panel.focused_border": null,
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#191724ff",
"search.match_background": "#57949f66",
"panel.background": "#1c1b2aff",
"panel.focused_border": null,
"panel.focused_border": "#9bced6ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#232132ff",
@@ -426,7 +426,7 @@
"tab.active_background": "#faf4edff",
"search.match_background": "#9cced766",
"panel.background": "#fef9f2ff",
"panel.focused_border": null,
"panel.focused_border": "#57949fff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#5752794c",
"scrollbar.thumb.hover_background": "#e5e0dfff",
@@ -806,7 +806,7 @@
"tab.active_background": "#232136ff",
"search.match_background": "#9cced766",
"panel.background": "#28253cff",
"panel.focused_border": null,
"panel.focused_border": "#9bced6ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#322f48ff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#282c33ff",
"search.match_background": "#528b8b66",
"panel.background": "#2b3038ff",
"panel.focused_border": null,
"panel.focused_border": "#518b8bff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf4c14c",
"scrollbar.thumb.hover_background": "#313741ff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#002a35ff",
"search.match_background": "#288bd166",
"panel.background": "#04313bff",
"panel.focused_border": null,
"panel.focused_border": "#278ad1ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf6e34c",
"scrollbar.thumb.hover_background": "#053541ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fdf6e3ff",
"search.match_background": "#298bd166",
"panel.background": "#f3eddaff",
"panel.focused_border": null,
"panel.focused_border": "#288bd1ff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#002a354c",
"scrollbar.thumb.hover_background": "#dcdacbff",

View File

@@ -46,7 +46,7 @@
"tab.active_background": "#1b1810ff",
"search.match_background": "#499bef66",
"panel.background": "#231f16ff",
"panel.focused_border": null,
"panel.focused_border": "#499befff",
"pane.focused_border": null,
"scrollbar.thumb.background": "#f8f5de4c",
"scrollbar.thumb.hover_background": "#29251bff",

View File

@@ -15,6 +15,7 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakView<Workspace>,
@@ -202,6 +203,8 @@ impl ActiveThread {
return Empty.into_any();
};
let context = self.thread.read(cx).context_for_message(message_id);
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
@@ -229,7 +232,16 @@ impl ActiveThread {
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
}),
)
.into_any()
}

View File

@@ -1,10 +1,12 @@
mod active_thread;
mod assistant_panel;
mod context;
mod context_picker;
mod message_editor;
mod thread;
mod thread_history;
mod thread_store;
mod ui;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};

View File

@@ -9,10 +9,8 @@ use gpui::{
WindowContext,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector;
use time::UtcOffset;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
@@ -21,7 +19,7 @@ use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
@@ -225,7 +223,6 @@ impl AssistantPanel {
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(self.render_language_model_selector(cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
@@ -280,57 +277,6 @@ impl AssistantPanel {
)
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
@@ -358,46 +304,6 @@ impl AssistantPanel {
.mb_4(),
),
)
.child(v_flex())
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Terminal)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("Terminal").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Folder)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.when(!recent_threads.is_empty(), |parent| {
parent
.child(

View File

@@ -0,0 +1,14 @@
use gpui::SharedString;
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub name: SharedString,
pub kind: ContextKind,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
}

View File

@@ -1,35 +1,46 @@
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use picker::Picker;
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenuHandle,
PopoverMenuHandle, Tooltip,
};
use crate::context::{Context, ContextKind};
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::thread::{RequestKind, Thread};
use crate::Chat;
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
context: Vec<Context>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
let mocked_context = vec![Context {
name: "shape.rs".into(),
kind: ContextKind::File,
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
}];
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything", cx);
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
editor
}),
context: mocked_context,
context_picker_handle: PopoverMenuHandle::default(),
use_tools: false,
}
@@ -61,9 +72,10 @@ impl MessageEditor {
editor.clear(cx);
text
});
let context = self.context.drain(..).collect::<Vec<_>>();
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, cx);
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
@@ -84,6 +96,57 @@ impl MessageEditor {
None
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
}
impl FocusableView for MessageEditor {
@@ -106,12 +169,32 @@ impl Render for MessageEditor {
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)),
h_flex()
.flex_wrap()
.gap_2()
.child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
))
.children(
self.context
.iter()
.map(|context| ContextPill::new(context.clone())),
)
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({
let settings = ThemeSettings::get_global(cx);
@@ -152,13 +235,12 @@ impl Render for MessageEditor {
.child(
h_flex()
.gap_2()
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
.child(Label::new("or"))
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),

View File

@@ -17,6 +17,8 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{Context, ContextKind};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
@@ -62,6 +64,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context_by_message: HashMap<MessageId, Vec<Context>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
@@ -79,6 +82,7 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
tools,
@@ -125,12 +129,22 @@ impl Thread {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
self.context_by_message.get(&id)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
self.insert_message(Role::User, text, cx)
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);
self.context_by_message.insert(message_id, context);
}
pub fn insert_message(
@@ -138,7 +152,7 @@ impl Thread {
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
) {
) -> MessageId {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
@@ -147,6 +161,7 @@ impl Thread {
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
id
}
pub fn to_completion_request(
@@ -176,6 +191,29 @@ impl Thread {
}
}
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push_str("\n");
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
context_text.push_str(&file_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
}
if !message.text.is_empty() {
request_message
.content

View File

@@ -159,9 +159,9 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
thread
}));
@@ -169,7 +169,7 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
```rust
@@ -206,7 +206,7 @@ impl ThreadStore {
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:

View File

@@ -0,0 +1,3 @@
mod context_pill;
pub use context_pill::*;

View File

@@ -0,0 +1,25 @@
use ui::prelude::*;
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
}
impl ContextPill {
pub fn new(context: Context) -> Self {
Self { context }
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
}
}

View File

@@ -18,11 +18,10 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]
livekit-macos = ["livekit_client_macos"]
livekit-cross-platform = ["livekit_client"]
[dependencies]
anyhow.workspace = true
@@ -42,8 +41,12 @@ serde.workspace = true
serde_derive.workspace = true
settings.workspace = true
util.workspace = true
livekit_client_macos = { workspace = true, optional = true }
livekit_client = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -1,41 +1,13 @@
pub mod call_settings;
#[cfg(any(
all(target_os = "macos", feature = "livekit-macos"),
all(
not(target_os = "macos"),
feature = "livekit-macos",
not(feature = "livekit-cross-platform")
)
))]
#[cfg(target_os = "macos")]
mod macos;
#[cfg(any(
all(target_os = "macos", feature = "livekit-macos"),
all(
not(target_os = "macos"),
feature = "livekit-macos",
not(feature = "livekit-cross-platform")
)
))]
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(any(
all(
target_os = "macos",
feature = "livekit-cross-platform",
not(feature = "livekit-macos"),
),
all(not(target_os = "macos"), feature = "livekit-cross-platform"),
))]
#[cfg(not(target_os = "macos"))]
mod cross_platform;
#[cfg(any(
all(
target_os = "macos",
feature = "livekit-cross-platform",
not(feature = "livekit-macos"),
),
all(not(target_os = "macos"), feature = "livekit-cross-platform"),
))]
#[cfg(not(target_os = "macos"))]
pub use cross_platform::*;

View File

@@ -18,7 +18,8 @@ use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -355,6 +356,24 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_rating_event(
self: &Arc<Self>,
rating: InlineCompletionRating,
input_events: Arc<str>,
input_excerpt: Arc<str>,
output_excerpt: Arc<str>,
feedback: String,
) {
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
rating,
input_events,
input_excerpt,
output_excerpt,
feedback,
});
self.report_event(event);
}
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}

View File

@@ -19,11 +19,6 @@ LLM_DATABASE_URL = "postgres://postgres@localhost/zed_llm"
LLM_DATABASE_MAX_CONNECTIONS = 5
LLM_API_SECRET = "llm-secret"
# CLICKHOUSE_URL = ""
# CLICKHOUSE_USER = "default"
# CLICKHOUSE_PASSWORD = ""
# CLICKHOUSE_DATABASE = "default"
# SLACK_PANICS_WEBHOOK = ""
# RUST_LOG=info

View File

@@ -29,7 +29,6 @@ axum = { version = "0.6", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.4", features = ["erased-json"] }
base64.workspace = true
chrono.workspace = true
clickhouse.workspace = true
clock.workspace = true
collections.workspace = true
dashmap.workspace = true
@@ -77,12 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true, features = ["test-support"] }
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_tool.workspace = true

View File

@@ -149,6 +149,21 @@ spec:
secretKeyRef:
name: google-ai
key: api_key
- name: PREDICTION_API_URL
valueFrom:
secretKeyRef:
name: prediction
key: api_url
- name: PREDICTION_API_KEY
valueFrom:
secretKeyRef:
name: prediction
key: api_key
- name: PREDICTION_MODEL
valueFrom:
secretKeyRef:
name: prediction
key: model
- name: BLOB_STORE_ACCESS_KEY
valueFrom:
secretKeyRef:
@@ -199,26 +214,6 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: CLICKHOUSE_URL
valueFrom:
secretKeyRef:
name: clickhouse
key: url
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse
key: user
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse
key: password
- name: CLICKHOUSE_DATABASE
valueFrom:
secretKeyRef:
name: clickhouse
key: database
- name: SLACK_PANICS_WEBHOOK
valueFrom:
secretKeyRef:

View File

@@ -9,6 +9,7 @@ use collections::HashSet;
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{str::FromStr, sync::Arc, time::Duration};
use stripe::{
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
@@ -19,6 +20,7 @@ use stripe::{
};
use util::ResultExt;
use crate::api::events::SnowflakeRow;
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::rpc::{ResultExt as _, Server};
use crate::{
@@ -100,6 +102,9 @@ async fn update_billing_preferences(
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let max_monthly_llm_usage_spending_in_cents =
body.max_monthly_llm_usage_spending_in_cents.max(0);
let billing_preferences =
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
app.db
@@ -107,7 +112,7 @@ async fn update_billing_preferences(
user.id,
&UpdateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
body.max_monthly_llm_usage_spending_in_cents,
max_monthly_llm_usage_spending_in_cents,
),
},
)
@@ -117,13 +122,26 @@ async fn update_billing_preferences(
.create_billing_preferences(
user.id,
&crate::db::CreateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: body
.max_monthly_llm_usage_spending_in_cents,
max_monthly_llm_usage_spending_in_cents,
},
)
.await?
};
SnowflakeRow::new(
"Spend Limit Updated",
user.metrics_id,
user.admin,
None,
json!({
"user_id": user.id,
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
}),
)
.write(&app.kinesis_client, &app.config.kinesis_stream)
.await
.log_err();
rpc_server.refresh_llm_tokens_for_user(user.id).await;
Ok(Json(BillingPreferencesResponse {

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
use serde::Serialize;
/// Writes the given rows to the specified Clickhouse table.
pub async fn write_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
}
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
Ok(())
}

View File

@@ -1,7 +1,6 @@
pub mod api;
pub mod auth;
mod cents;
pub mod clickhouse;
pub mod db;
pub mod env;
pub mod executor;
@@ -151,10 +150,6 @@ pub struct Config {
pub seed_path: Option<PathBuf>,
pub database_max_connections: u32,
pub api_token: String,
pub clickhouse_url: Option<String>,
pub clickhouse_user: Option<String>,
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub livekit_server: Option<String>,
pub livekit_key: Option<String>,
@@ -180,6 +175,9 @@ pub struct Config {
pub anthropic_api_key: Option<Arc<str>>,
pub anthropic_staff_api_key: Option<Arc<str>>,
pub llm_closed_beta_model_name: Option<Arc<str>>,
pub prediction_api_url: Option<Arc<str>>,
pub prediction_api_key: Option<Arc<str>>,
pub prediction_model: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
@@ -230,10 +228,9 @@ impl Config {
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
@@ -283,7 +280,6 @@ pub struct AppState {
pub stripe_billing: Option<Arc<StripeBilling>>,
pub rate_limiter: Arc<RateLimiter>,
pub executor: Executor,
pub clickhouse_client: Option<::clickhouse::Client>,
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
pub config: Config,
}
@@ -337,10 +333,6 @@ impl AppState {
stripe_client,
rate_limiter: Arc::new(RateLimiter::new(db)),
executor,
clickhouse_client: config
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
} else {
@@ -423,31 +415,3 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
Ok(::clickhouse::Client::default()
.with_url(
config
.clickhouse_url
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_url"))?,
)
.with_user(
config
.clickhouse_user
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_user"))?,
)
.with_password(
config
.clickhouse_password
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_password"))?,
)
.with_database(
config
.clickhouse_database
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_database"))?,
))
}

View File

@@ -1,14 +1,11 @@
mod authorization;
pub mod db;
mod telemetry;
mod token;
use crate::api::events::SnowflakeRow;
use crate::api::CloudflareIpCountryHeader;
use crate::build_kinesis_client;
use crate::{
build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result,
};
use crate::{db::UserId, executor::Executor, Cents, Config, Error, Result};
use anyhow::{anyhow, Context as _};
use authorization::authorize_access_to_language_model;
use axum::routing::get;
@@ -29,7 +26,10 @@ use reqwest_client::ReqwestClient;
use rpc::{
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
};
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
use rpc::{
ListModelsResponse, PredictEditsParams, PredictEditsResponse,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
};
use serde_json::json;
use std::{
pin::Pin,
@@ -37,7 +37,6 @@ use std::{
task::{Context, Poll},
};
use strum::IntoEnumIterator;
use telemetry::{report_llm_rate_limit, report_llm_usage, LlmRateLimitEventRow, LlmUsageEventRow};
use tokio::sync::RwLock;
use util::ResultExt;
@@ -49,7 +48,6 @@ pub struct LlmState {
pub db: Arc<LlmDatabase>,
pub http_client: ReqwestClient,
pub kinesis_client: Option<aws_sdk_kinesis::Client>,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
}
@@ -86,10 +84,6 @@ impl LlmState {
} else {
None
},
clickhouse_client: config
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
active_user_count_by_model: RwLock::new(HashMap::default()),
config,
};
@@ -126,6 +120,7 @@ pub fn routes() -> Router<(), Body> {
Router::new()
.route("/models", get(list_models))
.route("/completion", post(perform_completion))
.route("/predict_edits", post(predict_edits))
.layer(middleware::from_fn(validate_api_token))
}
@@ -439,6 +434,59 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
}
}
async fn predict_edits(
Extension(state): Extension<Arc<LlmState>>,
Extension(claims): Extension<LlmTokenClaims>,
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
Json(params): Json<PredictEditsParams>,
) -> Result<impl IntoResponse> {
if !claims.is_staff {
return Err(anyhow!("not found"))?;
}
let api_url = state
.config
.prediction_api_url
.as_ref()
.context("no PREDICTION_API_URL configured on the server")?;
let api_key = state
.config
.prediction_api_key
.as_ref()
.context("no PREDICTION_API_KEY configured on the server")?;
let model = state
.config
.prediction_model
.as_ref()
.context("no PREDICTION_MODEL configured on the server")?;
let prompt = include_str!("./llm/prediction_prompt.md")
.replace("<events>", &params.input_events)
.replace("<excerpt>", &params.input_excerpt);
let mut response = open_ai::complete_text(
&state.http_client,
api_url,
api_key,
open_ai::CompletionRequest {
model: model.to_string(),
prompt: prompt.clone(),
max_tokens: 1024,
temperature: 0.,
prediction: Some(open_ai::Prediction::Content {
content: params.input_excerpt,
}),
rewrite_speculation: Some(true),
},
)
.await?;
let choice = response
.choices
.pop()
.context("no output from completion response")?;
Ok(Json(PredictEditsResponse {
output_excerpt: choice.text,
}))
}
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
@@ -573,34 +621,6 @@ async fn check_usage_limit(
.await
.log_err();
if let Some(client) = state.clickhouse_client.as_ref() {
report_llm_rate_limit(
client,
LlmRateLimitEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model: model.name.clone(),
provider: provider.to_string(),
usage_measure: resource.to_string(),
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
users_in_recent_minutes: users_in_recent_minutes as u64,
users_in_recent_days: users_in_recent_days as u64,
max_requests_per_minute: per_user_max_requests_per_minute as u64,
max_tokens_per_minute: per_user_max_tokens_per_minute as u64,
max_tokens_per_day: per_user_max_tokens_per_day as u64,
},
)
.await
.log_err();
}
return Err(Error::http(
StatusCode::TOO_MANY_REQUESTS,
format!("Rate limit exceeded. Maximum {} reached.", resource),
@@ -687,6 +707,8 @@ impl<S> Drop for TokenCountingStream<S> {
);
let properties = json!({
"has_llm_subscription": claims.has_llm_subscription,
"max_monthly_spend_in_cents": claims.max_monthly_spend_in_cents,
"plan": match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
@@ -706,44 +728,6 @@ impl<S> Drop for TokenCountingStream<S> {
.write(&state.kinesis_client, &state.config.kinesis_stream)
.await
.log_err();
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model,
provider: provider.to_string(),
input_token_count: tokens.input as u64,
cache_creation_input_token_count: tokens.input_cache_creation as u64,
cache_read_input_token_count: tokens.input_cache_read as u64,
output_token_count: tokens.output as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.tokens_this_month.input as u64,
cache_creation_input_tokens_this_month: usage
.tokens_this_month
.input_cache_creation
as u64,
cache_read_input_tokens_this_month: usage
.tokens_this_month
.input_cache_read
as u64,
output_tokens_this_month: usage.tokens_this_month.output as u64,
spending_this_month: usage.spending_this_month.0 as u64,
lifetime_spending: usage.lifetime_spending.0 as u64,
},
)
.await
.log_err();
}
}
})
}

View File

@@ -0,0 +1,12 @@
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
### Events:
<events>
### Input:
<excerpt>
### Response:

View File

@@ -1,65 +0,0 @@
use anyhow::{Context, Result};
use serde::Serialize;
use crate::clickhouse::write_to_table;
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct LlmUsageEventRow {
pub time: i64,
pub user_id: i32,
pub is_staff: bool,
pub plan: String,
pub model: String,
pub provider: String,
pub input_token_count: u64,
pub cache_creation_input_token_count: u64,
pub cache_read_input_token_count: u64,
pub output_token_count: u64,
pub requests_this_minute: u64,
pub tokens_this_minute: u64,
pub tokens_this_day: u64,
pub input_tokens_this_month: u64,
pub cache_creation_input_tokens_this_month: u64,
pub cache_read_input_tokens_this_month: u64,
pub output_tokens_this_month: u64,
pub spending_this_month: u64,
pub lifetime_spending: u64,
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct LlmRateLimitEventRow {
pub time: i64,
pub user_id: i32,
pub is_staff: bool,
pub plan: String,
pub model: String,
pub provider: String,
pub usage_measure: String,
pub requests_this_minute: u64,
pub tokens_this_minute: u64,
pub tokens_this_day: u64,
pub users_in_recent_minutes: u64,
pub users_in_recent_days: u64,
pub max_requests_per_minute: u64,
pub max_tokens_per_minute: u64,
pub max_tokens_per_day: u64,
}
pub async fn report_llm_usage(client: &clickhouse::Client, row: LlmUsageEventRow) -> Result<()> {
const LLM_USAGE_EVENTS_TABLE: &str = "llm_usage_events";
write_to_table(LLM_USAGE_EVENTS_TABLE, &[row], client)
.await
.with_context(|| format!("failed to upload to table '{LLM_USAGE_EVENTS_TABLE}'"))?;
Ok(())
}
pub async fn report_llm_rate_limit(
client: &clickhouse::Client,
row: LlmRateLimitEventRow,
) -> Result<()> {
const LLM_RATE_LIMIT_EVENTS_TABLE: &str = "llm_rate_limit_events";
write_to_table(LLM_RATE_LIMIT_EVENTS_TABLE, &[row], client)
.await
.with_context(|| format!("failed to upload to table '{LLM_RATE_LIMIT_EVENTS_TABLE}'"))?;
Ok(())
}

View File

@@ -17,10 +17,8 @@ pub struct LlmTokenClaims {
pub exp: u64,
pub jti: String,
pub user_id: u64,
#[serde(default)]
pub system_id: Option<String>,
#[serde(default)]
pub metrics_id: Option<Uuid>,
pub metrics_id: Uuid,
pub github_user_login: String,
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
@@ -56,7 +54,7 @@ impl LlmTokenClaims {
jti: uuid::Uuid::new_v4().to_string(),
user_id: user.id.to_proto(),
system_id,
metrics_id: Some(user.metrics_id),
metrics_id: user.metrics_id,
github_user_login: user.github_login.clone(),
is_staff,
has_llm_closed_beta_feature_flag,

View File

@@ -518,7 +518,6 @@ impl TestServer {
stripe_billing: None,
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
executor,
clickhouse_client: None,
kinesis_client: None,
config: Config {
http_port: 0,
@@ -546,10 +545,9 @@ impl TestServer {
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,

View File

@@ -59,18 +59,21 @@ workspace.workspace = true
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
clock.workspace = true
indoc.workspace = true
serde_json.workspace = true
clock = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
node_runtime = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -1,14 +1,13 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
};
use settings::Settings;
use std::{path::Path, sync::Arc, time::Duration};
use std::{path::Path, time::Duration};
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -21,7 +20,6 @@ pub struct CopilotCompletionProvider {
pending_refresh: Task<Result<()>>,
pending_cycling_refresh: Task<Result<()>>,
copilot: Model<Copilot>,
telemetry: Option<Arc<Telemetry>>,
}
impl CopilotCompletionProvider {
@@ -35,15 +33,9 @@ impl CopilotCompletionProvider {
pending_refresh: Task::ready(Ok(())),
pending_cycling_refresh: Task::ready(Ok(())),
copilot,
telemetry: None,
}
}
pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
self.telemetry = Some(telemetry);
self
}
fn active_completion(&self) -> Option<&Completion> {
self.completions.get(self.active_completion_index)
}
@@ -190,23 +182,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
if self.active_completion().is_some() {
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_inline_completion_event(
Self::name().to_string(),
true,
self.file_extension.clone(),
);
}
}
}
}
fn discard(
&mut self,
should_report_inline_completion_event: bool,
cx: &mut ModelContext<Self>,
) {
fn discard(&mut self, cx: &mut ModelContext<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
@@ -220,24 +199,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
copilot.discard_completions(&self.completions, cx)
})
.detach_and_log_err(cx);
if should_report_inline_completion_event && self.active_completion().is_some() {
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_inline_completion_event(
Self::name().to_string(),
false,
self.file_extension.clone(),
);
}
}
}
fn active_completion_text<'a>(
&'a self,
fn suggest(
&mut self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
) -> Option<CompletionProposal> {
cx: &mut ModelContext<Self>,
) -> Option<InlineCompletion> {
let buffer_id = buffer.entity_id();
let buffer = buffer.read(cx);
let completion = self.active_completion()?;
@@ -267,13 +236,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
if completion_text.trim().is_empty() {
None
} else {
Some(CompletionProposal {
inlays: vec![InlayProposal::Suggestion(
cursor_position.bias_right(buffer),
completion_text.into(),
)],
text: completion_text.into(),
delete_range: None,
let position = cursor_position.bias_right(buffer);
Some(InlineCompletion {
edits: vec![(position..position, completion_text.into())],
})
}
} else {
@@ -359,7 +324,7 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
// Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
@@ -368,7 +333,7 @@ mod tests {
.unwrap()
.detach();
assert!(!editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
@@ -401,7 +366,7 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
@@ -434,12 +399,12 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
// When hiding the context menu, the Copilot suggestion becomes visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
@@ -449,7 +414,7 @@ mod tests {
executor.run_until_parked();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -467,25 +432,25 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should remove the active Copilot suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// After canceling, tabbing shouldn't insert the previously shown suggestion.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -493,25 +458,25 @@ mod tests {
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// AcceptInlineCompletion when there is an active suggestion inserts it.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// Hide suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
});
@@ -520,7 +485,7 @@ mod tests {
// we won't make it visible.
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
cx.update_editor(|editor, cx| {
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
});
@@ -545,19 +510,19 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
editor.tab(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Using AcceptInlineCompletion again accepts the suggestion.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
});
@@ -615,17 +580,17 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
// Accepting the first word of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
// Accepting next word should accept the non-word and copilot suggestion should be gone
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
});
@@ -657,11 +622,11 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
// Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -670,7 +635,7 @@ mod tests {
// Accepting next word should accept the next word and copilot suggestion should still exist
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -679,7 +644,7 @@ mod tests {
// Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -730,29 +695,29 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\nt\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
// Deleting across the original suggestion range invalidates it.
editor.backspace(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\nthree\n");
assert_eq!(editor.text(cx), "one\nthree\n");
// Undoing the deletion restores the suggestion.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
});
@@ -813,7 +778,7 @@ mod tests {
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
@@ -835,7 +800,7 @@ mod tests {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
@@ -844,7 +809,7 @@ mod tests {
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
editor.handle_input(" ", cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
@@ -855,7 +820,7 @@ mod tests {
// Ensure the new suggestion is displayed when the debounce timeout expires.
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
@@ -916,7 +881,7 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
});
@@ -943,7 +908,7 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
});
@@ -974,7 +939,7 @@ mod tests {
"On completion trigger input, the completions should be fetched and visible"
);
assert!(
!editor.has_active_inline_completion(cx),
!editor.has_active_inline_completion(),
"On completion trigger input, copilot suggestion should be dismissed"
);
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
@@ -998,7 +963,7 @@ mod tests {
"/test",
json!({
".env": "SECRET=something\n",
"README.md": "hello\n"
"README.md": "hello\nworld\nhow\nare\nyou\ntoday"
}),
)
.await;
@@ -1030,7 +995,7 @@ mod tests {
multibuffer.push_excerpts(
public_buffer.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 0),
context: Point::new(0, 0)..Point::new(6, 0),
primary: None,
}],
cx,
@@ -1038,6 +1003,7 @@ mod tests {
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {
@@ -1073,7 +1039,7 @@ mod tests {
_ = editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, cx);
});

View File

@@ -16,8 +16,8 @@ use editor::{
};
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
Render, SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
WeakView, WindowContext,
};
use language::{
@@ -46,6 +46,9 @@ use workspace::{
actions!(diagnostics, [Deploy, ToggleWarnings]);
struct IncludeWarnings(bool);
impl Global for IncludeWarnings {}
pub fn init(cx: &mut AppContext) {
ProjectDiagnosticsSettings::register(cx);
cx.observe_new_views(ProjectDiagnosticsEditor::register)
@@ -117,6 +120,7 @@ impl ProjectDiagnosticsEditor {
fn new_with_context(
context: u32,
include_warnings: bool,
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
@@ -134,27 +138,16 @@ impl ProjectDiagnosticsEditor {
language_server_id,
path,
} => {
let max_severity = this.max_severity();
let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path)
.into_iter().flatten()
.filter(|(server_id, _)| language_server_id == server_id)
.flat_map(|(_, diagnostics)| diagnostics)
.any(|diagnostic| diagnostic.diagnostic.severity <= max_severity);
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
if has_diagnostics_to_display {
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.update_stale_excerpts(cx);
}
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display");
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.update_stale_excerpts(cx);
}
}
_ => {}
@@ -186,19 +179,24 @@ impl ProjectDiagnosticsEditor {
}
})
.detach();
cx.observe_global::<IncludeWarnings>(|this, cx| {
this.include_warnings = cx.global::<IncludeWarnings>().0;
this.update_all_excerpts(cx);
})
.detach();
let project = project_handle.read(cx);
let mut this = Self {
project: project_handle.clone(),
context,
summary: project.diagnostic_summary(false, cx),
include_warnings,
workspace,
excerpts,
focus_handle,
editor,
path_states: Default::default(),
paths_to_update: Default::default(),
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
update_excerpts_task: None,
_subscription: project_event_subscription,
};
@@ -243,11 +241,13 @@ impl ProjectDiagnosticsEditor {
fn new(
project_handle: Model<Project>,
include_warnings: bool,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
Self::new_with_context(
editor::DEFAULT_MULTIBUFFER_CONTEXT,
include_warnings,
project_handle,
workspace,
cx,
@@ -259,8 +259,19 @@ impl ProjectDiagnosticsEditor {
workspace.activate_item(&existing, true, true, cx);
} else {
let workspace_handle = cx.view().downgrade();
let include_warnings = match cx.try_global::<IncludeWarnings>() {
Some(include_warnings) => include_warnings.0,
None => ProjectDiagnosticsSettings::get_global(cx).include_warnings,
};
let diagnostics = cx.new_view(|cx| {
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
ProjectDiagnosticsEditor::new(
workspace.project().clone(),
include_warnings,
workspace_handle,
cx,
)
});
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
}
@@ -268,6 +279,7 @@ impl ProjectDiagnosticsEditor {
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
self.include_warnings = !self.include_warnings;
cx.set_global(IncludeWarnings(self.include_warnings));
self.update_all_excerpts(cx);
cx.notify();
}
@@ -340,12 +352,16 @@ impl ProjectDiagnosticsEditor {
ExcerptId::min()
};
let max_severity = self.max_severity();
let path_state = &mut self.path_states[path_ix];
let mut new_group_ixs = Vec::new();
let mut blocks_to_add = Vec::new();
let mut blocks_to_remove = HashSet::default();
let mut first_excerpt_id = None;
let max_severity = if self.include_warnings {
DiagnosticSeverity::WARNING
} else {
DiagnosticSeverity::ERROR
};
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
.into_iter()
@@ -634,14 +650,6 @@ impl ProjectDiagnosticsEditor {
prev_path = Some(path);
}
}
fn max_severity(&self) -> DiagnosticSeverity {
if self.include_warnings {
DiagnosticSeverity::WARNING
} else {
DiagnosticSeverity::ERROR
}
}
}
impl FocusableView for ProjectDiagnosticsEditor {
@@ -740,7 +748,12 @@ impl Item for ProjectDiagnosticsEditor {
Self: Sized,
{
Some(cx.new_view(|cx| {
ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
ProjectDiagnosticsEditor::new(
self.project.clone(),
self.include_warnings,
self.workspace.clone(),
cx,
)
}))
}

View File

@@ -151,7 +151,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// Open the project diagnostics view while there are already diagnostics.
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
true,
project.clone(),
workspace.downgrade(),
cx,
)
});
let editor = view.update(cx, |view, _| view.editor.clone());
@@ -459,7 +465,13 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
let workspace = window.root(cx).unwrap();
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
true,
project.clone(),
workspace.downgrade(),
cx,
)
});
let editor = view.update(cx, |view, _| view.editor.clone());
@@ -720,7 +732,13 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
let workspace = window.root(cx).unwrap();
let mutated_view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
true,
project.clone(),
workspace.downgrade(),
cx,
)
});
workspace.update(cx, |workspace, cx| {
@@ -791,7 +809,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
updated_language_servers.insert(server_id);
project.update(cx, |project, cx| {
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
@@ -800,10 +818,12 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
&mut next_group_id,
&mut rng,
);
project
lsp_store
.update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -816,12 +836,33 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
log::info!("constructing reference diagnostics view");
let reference_view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
true,
project.clone(),
workspace.downgrade(),
cx,
)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
cx.run_until_parked();
let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
for ((path, language_server_id), diagnostics) in current_diagnostics {
for diagnostic in diagnostics {
let found_excerpt = reference_excerpts.iter().any(|info| {
let row_range = info.range.context.start.row..info.range.context.end.row;
info.path == path.strip_prefix("/test").unwrap()
&& info.language_server == language_server_id
&& row_range.contains(&diagnostic.range.start.0.row)
});
assert!(found_excerpt, "diagnostic not found in reference view");
}
}
assert_eq!(mutated_excerpts, reference_excerpts);
}

View File

@@ -85,6 +85,7 @@ unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -105,6 +105,7 @@ pub struct MoveDownByLines {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectUpByLines {
#[serde(default)]
@@ -166,6 +167,13 @@ pub struct SpawnNearestTask {
pub reveal: task::RevealStrategy,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)]
pub enum UuidVersion {
#[default]
V4,
V7,
}
impl_actions!(
editor,
[
@@ -271,6 +279,8 @@ gpui::actions!(
HalfPageUp,
Hover,
Indent,
InsertUuidV4,
InsertUuidV7,
JoinLines,
KillRingCut,
KillRingYank,

View File

@@ -0,0 +1,963 @@
use std::{
cell::Cell,
cmp::{min, Reverse},
ops::Range,
sync::Arc,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::{CodeAction, Completion, TaskSourceKind};
use std::iter;
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,
StatefulInteractiveElement as _, Styled, StyledExt as _,
};
use util::ResultExt as _;
use workspace::Workspace;
use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion},
display_map::DisplayPoint,
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
};
pub enum CodeContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
}
impl CodeContextMenu {
pub fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
CodeContextMenu::Completions(menu) => menu.select_first(provider, cx),
CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
}
true
} else {
false
}
}
pub fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx),
CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
}
true
} else {
false
}
}
pub fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
CodeContextMenu::Completions(menu) => menu.select_next(provider, cx),
CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
}
true
} else {
false
}
}
pub fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
CodeContextMenu::Completions(menu) => menu.select_last(provider, cx),
CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
}
true
} else {
false
}
}
pub fn visible(&self) -> bool {
match self {
CodeContextMenu::Completions(menu) => menu.visible(),
CodeContextMenu::CodeActions(menu) => menu.visible(),
}
}
pub fn render(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (ContextMenuOrigin, AnyElement) {
match self {
CodeContextMenu::Completions(menu) => (
ContextMenuOrigin::EditorPoint(cursor_position),
menu.render(style, max_height, workspace, cx),
),
CodeContextMenu::CodeActions(menu) => {
menu.render(cursor_position, style, max_height, cx)
}
}
}
}
pub enum ContextMenuOrigin {
EditorPoint(DisplayPoint),
GutterIndicator(DisplayRow),
}
#[derive(Clone, Debug)]
pub struct CompletionsMenu {
pub id: CompletionId,
sort_completions: bool,
pub initial_position: Anchor,
pub buffer: Model<Buffer>,
pub completions: Arc<RwLock<Box<[Completion]>>>,
match_candidates: Arc<[StringMatchCandidate]>,
pub matches: Arc<[StringMatch]>,
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
pub aside_was_displayed: Cell<bool>,
show_completion_documentation: bool,
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
sort_completions: bool,
show_completion_documentation: bool,
initial_position: Anchor,
buffer: Model<Buffer>,
completions: Box<[Completion]>,
aside_was_displayed: bool,
) -> Self {
let match_candidates = completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()].into(),
)
})
.collect();
Self {
id,
sort_completions,
initial_position,
buffer,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches: Vec::new().into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
aside_was_displayed: Cell::new(aside_was_displayed),
show_completion_documentation,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
pub fn new_snippet_choices(
id: CompletionId,
sort_completions: bool,
choices: &Vec<String>,
selection: Range<Anchor>,
buffer: Model<Buffer>,
) -> Self {
let completions = choices
.iter()
.map(|choice| Completion {
old_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),
runs: Default::default(),
filter_range: Default::default(),
},
server_id: LanguageServerId(usize::MAX),
documentation: None,
lsp_completion: Default::default(),
confirm: None,
})
.collect();
let match_candidates = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
.collect();
let matches = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatch {
candidate_id: id,
score: 1.,
positions: vec![],
string: completion.clone(),
})
.collect();
Self {
id,
sort_completions,
initial_position: selection.start,
buffer,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches,
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
aside_was_displayed: Cell::new(false),
show_completion_documentation: false,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(0, provider, cx);
}
fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.prev_match_index(), provider, cx);
}
fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.next_match_index(), provider, cx);
}
fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.matches.len() - 1, provider, cx);
}
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.matches.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.matches.len() {
self.selected_item + 1
} else {
0
}
}
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if !self.resolve_completions {
return;
}
let Some(provider) = provider else {
return;
};
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.lock().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let matches_range = if self.selected_item == 0 {
0..min(visible_count, self.matches.len())
} else if self.selected_item == self.matches.len() - 1 {
self.matches.len().saturating_sub(visible_count)..self.matches.len()
} else {
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
};
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
matches_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.matches.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
let candidate_ids = match self.completions.try_read() {
None => candidate_ids.collect::<Vec<usize>>(),
Some(completions) => candidate_ids
.filter(|i| completions[*i].documentation.is_none())
.collect::<Vec<usize>>(),
};
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
let candidate_ids = iter::once(selected_candidate_id)
.chain(
candidate_ids
.into_iter()
.filter(|id| *id != selected_candidate_id),
)
.collect::<Vec<usize>>();
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
candidate_ids,
self.completions.clone(),
cx,
);
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
.detach();
}
fn visible(&self) -> bool {
!self.matches.is_empty()
}
fn render(
&self,
style: &EditorStyle,
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.matches
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completions = self.completions.read();
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let mut len = completion.label.text.chars().count();
if let Some(Documentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
}
}
len
})
.map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item];
match &self.completions.read()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
Some(div().child("No documentation"))
}
_ => None,
}
} else {
None
};
let aside_contents = if let Some(multiline_docs) = multiline_docs {
Some(multiline_docs)
} else if self.aside_was_displayed.get() {
Some(div().child("Fetching documentation..."))
} else {
None
};
self.aside_was_displayed.set(aside_contents.is_some());
let aside_contents = aside_contents.map(|div| {
div.id("multiline_docs")
.max_h(max_height)
.flex_1()
.px_1p5()
.py_1()
.min_w(px(260.))
.max_w(px(640.))
.w(px(500.))
.overflow_y_scroll()
.occlude()
});
let last_rendered_range = self.last_rendered_range.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.lock().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.read();
matches[range]
.iter()
.enumerate()
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id];
let documentation = if show_completion_documentation {
&completion.documentation
} else {
&None
};
let highlights = gpui::combine_highlights(
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion.lsp_completion.deprecated.unwrap_or(false) {
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color = Some(cx.theme().colors().text_muted);
}
(range, highlight)
},
),
);
let completion_label = StyledText::new(completion.label.text.clone())
.with_highlights(&style.text, highlights);
let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation {
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
} else {
None
};
let color_swatch = completion
.color()
.map(|color| div().size_4().bg(color).rounded_sm());
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.selected(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(
&ConfirmCompletion {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}))
.start_slot::<Div>(color_swatch)
.child(h_flex().overflow_hidden().child(completion_label))
.end_slot::<Label>(documentation_label),
)
})
.collect()
},
)
.occlude()
.max_h(max_height)
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
Popover::new()
.child(list)
.when_some(aside_contents, |popover, aside_contents| {
popover.aside(aside_contents)
})
.into_any_element()
}
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
let mut matches = if let Some(query) = query {
fuzzy::match_strings(
&self.match_candidates,
query,
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
executor,
)
.await
} else {
self.match_candidates
.iter()
.enumerate()
.map(|(candidate_id, candidate)| StringMatch {
candidate_id,
score: Default::default(),
positions: Default::default(),
string: candidate.string.clone(),
})
.collect()
};
// Remove all candidates where the query's start does not match the start of any word in the candidate
if let Some(query) = query {
if let Some(query_start) = query.chars().next() {
matches.retain(|string_match| {
split_words(&string_match.string).any(|word| {
// Check that the first codepoint of the word as lowercase matches the first
// codepoint of the query as lowercase
word.chars()
.flat_map(|codepoint| codepoint.to_lowercase())
.zip(query_start.to_lowercase())
.all(|(word_cp, query_cp)| word_cp == query_cp)
})
});
}
}
let completions = self.completions.read();
if self.sort_completions {
matches.sort_unstable_by_key(|mat| {
// We do want to strike a balance here between what the language server tells us
// to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
// `Creat` and there is a local variable called `CreateComponent`).
// So what we do is: we bucket all matches into two buckets
// - Strong matches
// - Weak matches
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
// and the Weak matches are the rest.
//
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
// matches, we prefer language-server sort_text first.
//
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
// Rest of the matches(weak) can be sorted as language-server expects.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum MatchScore<'a> {
Strong {
score: Reverse<OrderedFloat<f64>>,
sort_text: Option<&'a str>,
sort_key: (usize, &'a str),
},
Weak {
sort_text: Option<&'a str>,
score: Reverse<OrderedFloat<f64>>,
sort_key: (usize, &'a str),
},
}
let completion = &completions[mat.candidate_id];
let sort_key = completion.sort_key();
let sort_text = completion.lsp_completion.sort_text.as_deref();
let score = Reverse(OrderedFloat(mat.score));
if mat.score >= 0.2 {
MatchScore::Strong {
score,
sort_text,
sort_key,
}
} else {
MatchScore::Weak {
sort_text,
score,
sort_key,
}
}
});
}
for mat in &mut matches {
let completion = &completions[mat.candidate_id];
mat.string.clone_from(&completion.label.text);
for position in &mut mat.positions {
*position += completion.label.filter_range.start;
}
}
drop(completions);
self.matches = matches.into();
self.selected_item = 0;
}
}
#[derive(Clone)]
pub struct AvailableCodeAction {
pub excerpt_id: ExcerptId,
pub action: CodeAction,
pub provider: Arc<dyn CodeActionProvider>,
}
#[derive(Clone)]
pub struct CodeActionContents {
pub tasks: Option<Arc<ResolvedTasks>>,
pub actions: Option<Arc<[AvailableCodeAction]>>,
}
impl CodeActionContents {
fn len(&self) -> usize {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
(Some(tasks), None) => tasks.templates.len(),
(None, Some(actions)) => actions.len(),
(None, None) => 0,
}
}
fn is_empty(&self) -> bool {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
(Some(tasks), None) => tasks.templates.is_empty(),
(None, Some(actions)) => actions.is_empty(),
(None, None) => true,
}
}
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
self.tasks
.iter()
.flat_map(|tasks| {
tasks
.templates
.iter()
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
})
.chain(self.actions.iter().flat_map(|actions| {
actions.iter().map(|available| CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
})
}))
}
pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => {
if index < tasks.templates.len() {
tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
} else {
actions.get(index - tasks.templates.len()).map(|available| {
CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
}
})
}
}
(Some(tasks), None) => tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
(None, Some(actions)) => {
actions
.get(index)
.map(|available| CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
})
}
(None, None) => None,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum CodeActionsItem {
Task(TaskSourceKind, ResolvedTask),
CodeAction {
excerpt_id: ExcerptId,
action: CodeAction,
provider: Arc<dyn CodeActionProvider>,
},
}
impl CodeActionsItem {
fn as_task(&self) -> Option<&ResolvedTask> {
let Self::Task(_, task) = self else {
return None;
};
Some(task)
}
fn as_code_action(&self) -> Option<&CodeAction> {
let Self::CodeAction { action, .. } = self else {
return None;
};
Some(action)
}
pub fn label(&self) -> String {
match self {
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
Self::Task(_, task) => task.resolved_label.clone(),
}
}
}
pub struct CodeActionsMenu {
pub actions: CodeActionContents,
pub buffer: Model<Buffer>,
pub selected_item: usize,
pub scroll_handle: UniformListScrollHandle,
pub deployed_from_indicator: Option<DisplayRow>,
}
impl CodeActionsMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify()
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.actions.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify();
}
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item + 1 < self.actions.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify();
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = self.actions.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify()
}
fn visible(&self) -> bool {
!self.actions.is_empty()
}
fn render(
&self,
cursor_position: DisplayPoint,
_style: &EditorStyle,
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> (ContextMenuOrigin, AnyElement) {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let element = uniform_list(
cx.view().clone(),
"code_actions_menu",
self.actions.len(),
move |_this, range, cx| {
actions
.iter()
.skip(range.start)
.take(range.end - range.start)
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
let selected = selected_item == item_ix;
let colors = cx.theme().colors();
div()
.px_1()
.rounded_md()
.text_color(colors.text)
.when(selected, |style| {
style
.bg(colors.element_active)
.text_color(colors.text_accent)
})
.hover(|style| {
style
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
.whitespace_nowrap()
.when_some(action.as_code_action(), |this, action| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(
action.lsp_action.title.replace("\n", ""),
))
})
.when_some(action.as_task(), |this, task| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
.child(SharedString::from(task.resolved_label.replace("\n", "")))
})
})
.collect()
},
)
.elevation_1(cx)
.p_1()
.max_h(max_height)
.occlude()
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(
self.actions
.iter()
.enumerate()
.max_by_key(|(_, action)| match action {
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title.chars().count()
}
})
.map(|(ix, _)| ix),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.into_any_element();
let cursor_position = if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
(cursor_position, element)
}
}

View File

@@ -1,46 +0,0 @@
use std::time::Duration;
use futures::{channel::oneshot, FutureExt};
use gpui::{Task, ViewContext};
use crate::Editor;
#[derive(Debug)]
pub struct DebouncedDelay {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DebouncedDelay {
pub fn new() -> DebouncedDelay {
DebouncedDelay {
task: None,
cancel_channel: None,
}
}
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Editor>, func: F)
where
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
}
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
drop(self.task.take());
self.task = Some(cx.spawn(move |model, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse();
futures::select_biased! {
_ = receiver => return,
_ = timer => {}
}
if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) {
task.await;
}
}));
}
}

View File

@@ -82,7 +82,7 @@ pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type TextHighlights = TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
@@ -434,7 +434,7 @@ impl DisplayMap {
style: HighlightStyle,
) {
self.text_highlights
.insert(Some(type_id), Arc::new((style, ranges)));
.insert(type_id, Arc::new((style, ranges)));
}
pub(crate) fn highlight_inlays(
@@ -457,11 +457,11 @@ impl DisplayMap {
}
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
let highlights = self.text_highlights.get(&Some(type_id))?;
let highlights = self.text_highlights.get(&type_id)?;
Some((highlights.0, &highlights.1))
}
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
let mut cleared = self.text_highlights.remove(&type_id).is_some();
cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@@ -1125,6 +1125,12 @@ impl DisplaySnapshot {
DisplayRow(self.block_snapshot.longest_row())
}
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
let block_range = BlockRow(range.start.0)..BlockRow(range.end.0);
let longest_row = self.block_snapshot.longest_row_in_range(block_range);
DisplayRow(longest_row.0)
}
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_row();
if buffer_row >= max_row {
@@ -1239,7 +1245,7 @@ impl DisplaySnapshot {
&self,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
let type_id = TypeId::of::<Tag>();
self.text_highlights.get(&Some(type_id)).cloned()
self.text_highlights.get(&type_id).cloned()
}
#[allow(unused)]

View File

@@ -1339,6 +1339,57 @@ impl BlockSnapshot {
self.transforms.summary().longest_row
}
pub fn longest_row_in_range(&self, range: Range<BlockRow>) -> BlockRow {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&range.start, Bias::Right, &());
let mut longest_row = range.start;
let mut longest_row_chars = 0;
if let Some(transform) = cursor.item() {
if transform.block.is_none() {
let (output_start, input_start) = cursor.start();
let overshoot = range.start.0 - output_start.0;
let wrap_start_row = input_start.0 + overshoot;
let wrap_end_row = cmp::min(
input_start.0 + (range.end.0 - output_start.0),
cursor.end(&()).1 .0,
);
let summary = self
.wrap_snapshot
.text_summary_for_range(wrap_start_row..wrap_end_row);
longest_row = BlockRow(range.start.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
}
cursor.next(&());
}
let cursor_start_row = cursor.start().0;
if range.end > cursor_start_row {
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &());
if summary.longest_row_chars > longest_row_chars {
longest_row = BlockRow(cursor_start_row.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
}
if let Some(transform) = cursor.item() {
if transform.block.is_none() {
let (output_start, input_start) = cursor.start();
let overshoot = range.end.0 - output_start.0;
let wrap_start_row = input_start.0;
let wrap_end_row = input_start.0 + overshoot;
let summary = self
.wrap_snapshot
.text_summary_for_range(wrap_start_row..wrap_end_row);
if summary.longest_row_chars > longest_row_chars {
longest_row = BlockRow(output_start.0 + summary.longest_row);
}
}
}
}
longest_row
}
pub(super) fn line_len(&self, row: BlockRow) -> u32 {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(row.0), Bias::Right, &());
@@ -2705,6 +2756,40 @@ mod tests {
longest_line_len,
);
for _ in 0..10 {
let end_row = rng.gen_range(1..=expected_lines.len());
let start_row = rng.gen_range(0..end_row);
let mut expected_longest_rows_in_range = vec![];
let mut longest_line_len_in_range = 0;
let mut row = start_row as u32;
for line in &expected_lines[start_row..end_row] {
let line_char_count = line.chars().count() as isize;
match line_char_count.cmp(&longest_line_len_in_range) {
Ordering::Less => {}
Ordering::Equal => expected_longest_rows_in_range.push(row),
Ordering::Greater => {
longest_line_len_in_range = line_char_count;
expected_longest_rows_in_range.clear();
expected_longest_rows_in_range.push(row);
}
}
row += 1;
}
let longest_row_in_range = blocks_snapshot
.longest_row_in_range(BlockRow(start_row as u32)..BlockRow(end_row as u32));
assert!(
expected_longest_rows_in_range.contains(&longest_row_in_range.0),
"incorrect longest row {} in range {:?}. expected {:?} with length {}",
longest_row,
start_row..end_row,
expected_longest_rows_in_range,
longest_line_len_in_range,
);
}
// Ensure that conversion between block points and wrap points is stable.
for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
let wrap_point = WrapPoint::new(row, 0);

View File

@@ -211,7 +211,7 @@ pub struct InlayBufferRows<'a> {
struct HighlightEndpoint {
offset: InlayOffset,
is_start: bool,
tag: Option<TypeId>,
tag: TypeId,
style: HighlightStyle,
}
@@ -239,7 +239,7 @@ pub struct InlayChunks<'a> {
max_output_offset: InlayOffset,
highlight_styles: HighlightStyles,
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
active_highlights: BTreeMap<TypeId, HighlightStyle>,
highlights: Highlights<'a>,
snapshot: &'a InlaySnapshot,
}
@@ -1096,7 +1096,7 @@ impl InlaySnapshot {
&self,
cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
range: &Range<InlayOffset>,
text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
text_highlights: &TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
highlight_endpoints: &mut Vec<HighlightEndpoint>,
) {
while cursor.start().0 < range.end {
@@ -1112,7 +1112,7 @@ impl InlaySnapshot {
)))
};
for (tag, text_highlights) in text_highlights.iter() {
for (&tag, text_highlights) in text_highlights.iter() {
let style = text_highlights.0;
let ranges = &text_highlights.1;
@@ -1134,13 +1134,13 @@ impl InlaySnapshot {
highlight_endpoints.push(HighlightEndpoint {
offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
is_start: true,
tag: *tag,
tag,
style,
});
highlight_endpoints.push(HighlightEndpoint {
offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
is_start: false,
tag: *tag,
tag,
style,
});
}
@@ -1708,7 +1708,7 @@ mod tests {
text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
log::info!("highlighting text ranges {text_highlight_ranges:?}");
text_highlights.insert(
Some(TypeId::of::<()>()),
TypeId::of::<()>(),
Arc::new((
HighlightStyle::default(),
text_highlight_ranges

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,6 @@ pub struct EditorSettings {
pub cursor_shape: Option<CursorShape>,
pub current_line_highlight: CurrentLineHighlight,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub completion_documentation_secondary_query_debounce: u64,
pub toolbar: Toolbar,
pub scrollbar: Scrollbar,
pub gutter: Gutter,
@@ -194,21 +191,6 @@ pub struct EditorSettingsContent {
/// Default: true
pub hover_popover_enabled: Option<bool>,
/// Whether to pop the completions menu while typing in an editor without
/// explicitly requesting it.
///
/// Default: true
pub show_completions_on_input: Option<bool>,
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
///
/// Default: true
pub show_completion_documentation: Option<bool>,
/// The debounce delay before re-querying the language server for completion
/// documentation when not included in original completion list.
///
/// Default: 300 ms
pub completion_documentation_secondary_query_debounce: Option<u64>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings

View File

@@ -9,8 +9,8 @@ use crate::{
};
use futures::StreamExt;
use gpui::{
div, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds,
WindowOptions,
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
WindowBounds, WindowOptions,
};
use indoc::indoc;
use language::{
@@ -25,15 +25,15 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{self, AtomicBool};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::sync::atomic::{self, AtomicUsize};
use std::{cell::RefCell, future::Future, iter, rc::Rc, time::Instant};
use test::editor_lsp_test_context::rust_lang;
use unindent::Unindent;
use util::{
@@ -8376,12 +8376,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.show_completions_on_input = Some(false);
});
})
update_test_language_settings(&mut cx, |settings| {
settings.defaults.show_completions_on_input = Some(false);
});
cx.set_state("editorˇ");
cx.simulate_keystroke(".");
@@ -8447,7 +8443,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["first", "last"]
@@ -8459,7 +8455,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_down(&MovePageDown::default(), cx);
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 1,
"expected PageDown to select the last item from the context menu"
@@ -8471,7 +8467,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_up(&MovePageUp::default(), cx);
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 0,
"expected PageUp to select the first item from the context menu"
@@ -8539,7 +8535,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["r", "ret", "Range", "return"]
@@ -9927,7 +9923,8 @@ async fn go_to_prev_overlapping_diagnostic(
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
let lsp_store =
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
@@ -9935,8 +9932,8 @@ async fn go_to_prev_overlapping_diagnostic(
"});
cx.update(|cx| {
project.update(cx, |project, cx| {
project
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
@@ -10025,11 +10022,12 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
fn func(abˇc def: i32) -> u32 {
}
"});
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
let lsp_store =
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.update(|cx| {
project.update(cx, |project, cx| {
project.update_diagnostics(
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/root/file").unwrap(),
@@ -10670,12 +10668,12 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
}
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
@@ -10701,7 +10699,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(
@@ -10710,7 +10708,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
"Should update the completion label after resolving"
);
}
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
}
@@ -10719,6 +10717,62 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
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 {
@@ -10735,138 +10789,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.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();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
items,
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10883,51 +10814,76 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
cx.update_editor(|editor, _| {
let menu = editor.context_menu.read();
match menu.as_ref().expect("should have the completions menu") {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
assert_eq!(
completions_menu
.matches
.iter()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
.map(|c| c.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
);
}
ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_first(&ContextMenuFirst, cx);
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
}
#[gpui::test]
@@ -10992,7 +10948,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
@@ -11005,7 +10961,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
@@ -11021,7 +10977,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]

View File

@@ -1,5 +1,6 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
code_context_menus::CodeActionsMenu,
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
},
@@ -16,13 +17,13 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
@@ -31,7 +32,7 @@ use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
@@ -47,7 +48,10 @@ use language::{
ChunkRendererContext,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
ProjectPath,
@@ -456,6 +460,8 @@ impl EditorElement {
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::reload_file);
register_action(view, cx, Editor::spawn_nearest_task);
register_action(view, cx, Editor::insert_uuid_v4);
register_action(view, cx, Editor::insert_uuid_v7);
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
@@ -1675,7 +1681,7 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
actions,
..
@@ -1690,16 +1696,23 @@ impl EditorElement {
None
};
let offset_range_start = snapshot
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
.to_offset(&snapshot.buffer_snapshot);
let offset_range_end = snapshot
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
.to_offset(&snapshot.buffer_snapshot);
editor
.tasks
.iter()
.filter_map(|(_, tasks)| {
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let display_row = multibuffer_point.to_display_point(snapshot).row();
if range.start > display_row || range.end < display_row {
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
return None;
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold.
if multibuffer_row
@@ -1712,6 +1725,7 @@ impl EditorElement {
return None;
}
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
let button = editor.render_run_indicator(
&self.style,
Some(display_row) == active_task_indicator_row,
@@ -1750,7 +1764,7 @@ impl EditorElement {
let mut button = None;
let row = newest_selection_head.row();
self.editor.update(cx, |editor, cx| {
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
..
})) = editor.context_menu.read().as_ref()
@@ -2718,6 +2732,156 @@ impl EditorElement {
true
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_completion_popover(
&self,
text_bounds: &Bounds<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
editor_width: Pixels,
style: &EditorStyle,
cx: &mut WindowContext,
) -> Option<AnyElement> {
const PADDING_X: Pixels = Pixels(25.);
const PADDING_Y: Pixels = Pixels(2.);
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
match &active_inline_completion.completion {
InlineCompletion::Move(target_position) => {
let container_element = div()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.px_1();
let target_display_point = target_position.to_display_point(editor_snapshot);
if target_display_point.row().as_f32() < scroll_top {
let mut element = container_element
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowUp)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
element.prepaint_at(text_bounds.origin + offset, cx);
Some(element)
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = container_element
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowDown)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height - size.height - PADDING_Y,
);
element.prepaint_at(text_bounds.origin + offset, cx);
Some(element)
} else {
let mut element = container_element
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit")),
)
.into_any();
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let origin = self.editor.update(cx, |editor, cx| {
editor.display_to_pixel_point(target_line_end, editor_snapshot, cx)
})?;
element.prepaint_as_root(
text_bounds.origin + origin + point(PADDING_X, px(0.)),
AvailableSpace::min_size(),
cx,
);
Some(element)
}
}
InlineCompletion::Edit(edits) => {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
if all_edits_insertions_or_deletions(edits, &editor_snapshot.buffer_snapshot) {
return None;
}
let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
cx,
)
.width
};
let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
let mut element = div()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.px_1()
.child(text)
.into_any();
let origin = text_bounds.origin
+ point(
longest_line_width + PADDING_X - scroll_pixel_position.x,
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
);
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
Some(element)
}
}
}
fn layout_mouse_context_menu(
&self,
editor_snapshot: &EditorSnapshot,
@@ -3940,6 +4104,16 @@ impl EditorElement {
}
}
fn paint_inline_completion_popover(
&mut self,
layout: &mut EditorLayout,
cx: &mut WindowContext,
) {
if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
inline_completion_popover.paint(cx);
}
}
fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
mouse_context_menu.paint(cx);
@@ -4132,6 +4306,89 @@ impl EditorElement {
}
}
fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
cx: &WindowContext,
) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let mut text = String::new();
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
let mut highlights = Vec::new();
for (old_range, new_text) in edits {
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..old_offset_range.start, false)
.map(|chunk| chunk.text),
);
offset = old_offset_range.end;
let start = text.len();
text.push_str(new_text);
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
}
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
.to_offset(editor_snapshot, Bias::Right);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..end_of_line, false)
.map(|chunk| chunk.text),
);
(text, highlights)
}
fn all_edits_insertions_or_deletions(
edits: &Vec<(Range<Anchor>, String)>,
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut all_insertions = true;
let mut all_deletions = true;
for (range, new_text) in edits.iter() {
let range_is_empty = range.to_offset(&snapshot).is_empty();
let text_is_empty = new_text.is_empty();
if range_is_empty != text_is_empty {
if range_is_empty {
all_deletions = false;
} else {
all_insertions = false;
}
} else {
return false;
}
if !all_insertions && !all_deletions {
return false;
}
}
all_insertions || all_deletions
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_button(
button: IconButton,
@@ -5564,6 +5821,20 @@ impl Element for EditorElement {
);
}
let inline_completion_popover = self.layout_inline_completion_popover(
&text_hitbox.bounds,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
editor_width,
&style,
cx,
);
let mouse_context_menu = self.layout_mouse_context_menu(
&snapshot,
start_row..end_row,
@@ -5650,6 +5921,7 @@ impl Element for EditorElement {
cursors,
visible_cursors,
selections,
inline_completion_popover,
mouse_context_menu,
test_indicators,
code_actions_indicator,
@@ -5739,6 +6011,7 @@ impl Element for EditorElement {
}
self.paint_scrollbar(layout, cx);
self.paint_inline_completion_popover(layout, cx);
self.paint_mouse_context_menu(layout, cx);
});
})
@@ -5794,6 +6067,7 @@ pub struct EditorLayout {
test_indicators: Vec<AnyElement>,
crease_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
inline_completion_popover: Option<AnyElement>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
@@ -6835,6 +7109,161 @@ mod tests {
}
}
#[gpui::test]
fn test_inline_completion_popover_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// Test case 1: Simple insertion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
let edits = vec![(edit_range, " beautiful".to_string())];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..16);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 2: Replacement
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("This is a test.", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
"That".to_string(),
)];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 0..4);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 3: Multiple edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
"Greetings".into(),
),
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
" and universe".into(),
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Greetings, world and universe!");
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].0, 0..9);
assert_eq!(highlights[1].0, 16..29);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
assert_eq!(
highlights[1].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 4: Multiple lines with edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
"First line\nSecond line\nThird line\nFourth line",
cx,
);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
"modified".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
"New third line".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
" updated".to_string(),
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
assert_eq!(highlights.len(), 3);
assert_eq!(highlights[0].0, 7..15); // "modified"
assert_eq!(highlights[1].0, 16..30); // "New third line"
assert_eq!(highlights[2].0, 37..45); // " updated"
for highlight in &highlights {
assert_eq!(
highlight.1.background_color,
Some(cx.theme().status().created_background)
);
}
})
.unwrap();
}
}
fn collect_invisibles_from_new_editor(
cx: &mut TestAppContext,
editor_mode: EditorMode,

View File

@@ -6,7 +6,7 @@ use std::{
time::Duration,
};
use anyhow::Context as _;
use anyhow::{anyhow, Context as _};
use collections::{BTreeMap, HashMap};
use feature_flags::FeatureFlagAppExt;
use git::{
@@ -235,22 +235,30 @@ impl ProjectDiffEditor {
>::default();
let mut change_sets = Vec::new();
for (status, entry_id, entry_path, open_task) in open_tasks {
let (_, opened_model) = open_task.await.with_context(|| {
format!("loading buffer {:?} for git diff", entry_path.path)
})?;
let buffer = match opened_model.downcast::<Buffer>() {
Ok(buffer) => buffer,
Err(_model) => anyhow::bail!(
"Could not load {:?} as a buffer for git diff",
entry_path.path
),
let Some(buffer) = open_task
.await
.and_then(|(_, opened_model)| {
opened_model
.downcast::<Buffer>()
.map_err(|_| anyhow!("Unexpected non-buffer"))
})
.with_context(|| {
format!("loading {:?} for git diff", entry_path.path)
})
.log_err()
else {
continue;
};
let change_set = project
let Some(change_set) = project
.update(&mut cx, |project, cx| {
project.open_unstaged_changes(buffer.clone(), cx)
})?
.await?;
.await
.log_err()
else {
continue;
};
cx.update(|cx| {
buffers.insert(
@@ -267,7 +275,7 @@ impl ProjectDiffEditor {
new_entries.push((entry_path, entry_id));
}
Ok((buffers, new_entries, change_sets))
anyhow::Ok((buffers, new_entries, change_sets))
})
.await
.log_err()
@@ -305,11 +313,11 @@ impl ProjectDiffEditor {
project_diff_editor
.update(&mut cx, |project_diff_editor, cx| {
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
for change_set in change_sets {
project_diff_editor.editor.update(cx, |editor, cx| {
project_diff_editor.editor.update(cx, |editor, cx| {
for change_set in change_sets {
editor.diff_map.add_change_set(change_set, cx)
});
}
}
});
})
.ok();
}),

View File

@@ -89,7 +89,6 @@ impl DiffMap {
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
self.diff_bases.insert(
buffer_id,
DiffBaseState {
@@ -105,6 +104,7 @@ impl DiffMap {
change_set,
},
);
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
}
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {

View File

@@ -0,0 +1,360 @@
use gpui::Model;
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use text::{Point, ToOffset};
use ui::Context;
use crate::{
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
};
#[gpui::test]
async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let absolute_zero_celsius = ˇ;");
propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "-273.15");
});
accept_completion(&mut cx);
cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
}
#[gpui::test]
async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let pi = ˇ\"foo\";");
propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "3.14159");
});
accept_completion(&mut cx);
cx.assert_editor_state("let pi = 3.14159ˇ;")
}
#[gpui::test]
async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
// Cursor is 2+ lines above the proposed edit
cx.set_state(indoc! {"
line 0
line ˇ1
line 2
line 3
line
"});
propose_edits(
&provider,
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
});
// When accepting, cursor is moved to the proposed location
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
line 0
line 1
line 2
line 3
linˇe
"});
// Cursor is 2+ lines below the proposed edit
cx.set_state(indoc! {"
line 0
line
line 2
line 3
line ˇ4
"});
propose_edits(
&provider,
vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
});
// When accepting, cursor is moved to the proposed location
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
line 0
linˇe
line 2
line 3
line 4
"});
}
#[gpui::test]
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
// Cursor is 3+ lines above the proposed edit
cx.set_state(indoc! {"
line 0
line ˇ1
line 2
line 3
line 4
line
"});
let edit_location = Point::new(5, 3);
propose_edits(
&provider,
vec![(edit_location..edit_location, " 5")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *towards* the completion, it stays active
cx.set_selections_state(indoc! {"
line 0
line 1
line ˇ2
line 3
line 4
line
"});
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *away* from the completion, it is discarded
cx.set_selections_state(indoc! {"
line ˇ0
line 1
line 2
line 3
line 4
line
"});
cx.editor(|editor, _| {
assert!(editor.active_inline_completion.is_none());
});
// Cursor is 3+ lines below the proposed edit
cx.set_state(indoc! {"
line
line 1
line 2
line 3
line ˇ4
line 5
"});
let edit_location = Point::new(0, 3);
propose_edits(
&provider,
vec![(edit_location..edit_location, " 0")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *towards* the completion, it stays active
cx.set_selections_state(indoc! {"
line
line 1
line 2
line ˇ3
line 4
line 5
"});
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *away* from the completion, it is discarded
cx.set_selections_state(indoc! {"
line
line 1
line 2
line 3
line 4
line ˇ5
"});
cx.editor(|editor, _| {
assert!(editor.active_inline_completion.is_none());
});
}
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
) {
cx.editor(|editor, cx| {
let completion_state = editor
.active_inline_completion
.as_ref()
.expect("editor has no active completion");
if let InlineCompletion::Edit(edits) = &completion_state.completion {
assert(editor.buffer().read(cx).snapshot(cx), edits);
} else {
panic!("expected edit completion");
}
})
}
fn assert_editor_active_move_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, Anchor),
) {
cx.editor(|editor, cx| {
let completion_state = editor
.active_inline_completion
.as_ref()
.expect("editor has no active completion");
if let InlineCompletion::Move(anchor) = &completion_state.completion {
assert(editor.buffer().read(cx).snapshot(cx), *anchor);
} else {
panic!("expected move completion");
}
})
}
fn accept_completion(cx: &mut EditorTestContext) {
cx.update_editor(|editor, cx| {
editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx)
})
}
fn propose_edits<T: ToOffset>(
provider: &Model<FakeInlineCompletionProvider>,
edits: Vec<(Range<T>, &str)>,
cx: &mut EditorTestContext,
) {
let snapshot = cx.buffer_snapshot();
let edits = edits.into_iter().map(|(range, text)| {
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
(range, text.into())
});
cx.update(|cx| {
provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
edits: edits.collect(),
}))
})
});
}
fn assign_editor_completion_provider(
provider: Model<FakeInlineCompletionProvider>,
cx: &mut EditorTestContext,
) {
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(provider), cx);
})
}
#[derive(Default, Clone)]
struct FakeInlineCompletionProvider {
completion: Option<inline_completion::InlineCompletion>,
}
impl FakeInlineCompletionProvider {
pub fn set_inline_completion(
&mut self,
completion: Option<inline_completion::InlineCompletion>,
) {
self.completion = completion;
}
}
impl InlineCompletionProvider for FakeInlineCompletionProvider {
fn name() -> &'static str {
"fake-completion-provider"
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &gpui::AppContext,
) -> bool {
true
}
fn refresh(
&mut self,
_buffer: gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_debounce: bool,
_cx: &mut gpui::ModelContext<Self>,
) {
}
fn cycle(
&mut self,
_buffer: gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_direction: inline_completion::Direction,
_cx: &mut gpui::ModelContext<Self>,
) {
}
fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
fn suggest<'a>(
&mut self,
_buffer: &gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &mut gpui::ModelContext<Self>,
) -> Option<inline_completion::InlineCompletion> {
self.completion.clone()
}
}

View File

@@ -37,7 +37,7 @@ where
.find_map(|(trigger_anchor, language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some((

View File

@@ -24,6 +24,8 @@ interface github {
}
/// Returns the latest release for the given GitHub repository.
///
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Returns the GitHub release with the specified tag name for the given GitHub repository.

View File

@@ -32,7 +32,7 @@ use gpui::{
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope,
QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
@@ -1387,6 +1387,7 @@ impl ExtensionStore {
fn prepare_remote_extension(
&mut self,
extension_id: Arc<str>,
is_dev: bool,
tmp_dir: PathBuf,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
@@ -1397,26 +1398,45 @@ impl ExtensionStore {
};
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
if fs.is_file(&src_dir.join(well_known_path)).await {
fs.copy_file(
&src_dir.join(well_known_path),
&tmp_dir.join(well_known_path),
fs::CopyOptions::default(),
)
.await?
}
const EXTENSION_TOML: &str = "extension.toml";
const EXTENSION_WASM: &str = "extension.wasm";
const CONFIG_TOML: &str = "config.toml";
if is_dev {
let manifest_toml = toml::to_string(&loaded_extension.manifest)?;
fs.save(
&tmp_dir.join(EXTENSION_TOML),
&Rope::from(manifest_toml),
language::LineEnding::Unix,
)
.await?;
} else {
fs.copy_file(
&src_dir.join(EXTENSION_TOML),
&tmp_dir.join(EXTENSION_TOML),
fs::CopyOptions::default(),
)
.await?
}
if fs.is_file(&src_dir.join(EXTENSION_WASM)).await {
fs.copy_file(
&src_dir.join(EXTENSION_WASM),
&tmp_dir.join(EXTENSION_WASM),
fs::CopyOptions::default(),
)
.await?
}
for language_path in loaded_extension.manifest.languages.iter() {
if fs
.is_file(&src_dir.join(language_path).join("config.toml"))
.is_file(&src_dir.join(language_path).join(CONFIG_TOML))
.await
{
fs.create_dir(&tmp_dir.join(language_path)).await?;
fs.copy_file(
&src_dir.join(language_path).join("config.toml"),
&tmp_dir.join(language_path).join("config.toml"),
&src_dir.join(language_path).join(CONFIG_TOML),
&tmp_dir.join(language_path).join(CONFIG_TOML),
fs::CopyOptions::default(),
)
.await?
@@ -1462,6 +1482,7 @@ impl ExtensionStore {
this.update(cx, |this, cx| {
this.prepare_remote_extension(
missing_extension.id.clone().into(),
missing_extension.dev,
tmp_dir.path().to_owned(),
cx,
)
@@ -1476,6 +1497,11 @@ impl ExtensionStore {
})?
.await?;
log::info!(
"Finished uploading extension {}",
missing_extension.clone().id
);
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {

View File

@@ -59,6 +59,11 @@ impl FeatureFlag for ToolUseFeatureFlag {
}
}
pub struct ZetaFeatureFlag;
impl FeatureFlag for ZetaFeatureFlag {
const NAME: &'static str = "zeta";
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";

View File

@@ -21,6 +21,7 @@ gpui.workspace = true
http_client.workspace = true
log.workspace = true
parking_lot.workspace = true
regex.workspace = true
rope.workspace = true
serde.workspace = true
smol.workspace = true

View File

@@ -1,17 +1,23 @@
use std::sync::LazyLock;
use derive_more::Deref;
use regex::Regex;
use url::Url;
/// The URL to a Git remote.
#[derive(Debug, PartialEq, Eq, Clone, Deref)]
pub struct RemoteUrl(Url);
static USERNAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[0-9a-zA-Z\-_]+@").expect("Failed to create USERNAME_REGEX"));
impl std::str::FromStr for RemoteUrl {
type Err = url::ParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input.starts_with("git@") {
if USERNAME_REGEX.is_match(input) {
// Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git`
let ssh_url = input.replacen(':', "/", 1).replace("git@", "ssh://git@");
let ssh_url = format!("ssh://{}", input.replacen(':', "/", 1));
Ok(RemoteUrl(Url::parse(&ssh_url)?))
} else {
Ok(RemoteUrl(Url::parse(input)?))
@@ -40,6 +46,12 @@ mod tests {
"github.com",
"/octocat/zed.git",
),
(
"org-000000@github.com:octocat/zed.git",
"ssh",
"github.com",
"/octocat/zed.git",
),
(
"ssh://git@github.com/octocat/zed.git",
"ssh",

34
crates/git_ui/Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[package]
name = "git_ui"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
name = "git_ui"
path = "src/git_ui.rs"
[dependencies]
gpui.workspace = true
itertools = { workspace = true, optional = true }
menu.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
workspace.workspace = true
ui.workspace = true
project.workspace = true
smallvec.workspace = true
git.workspace = true
editor.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[features]
default = []
stories = ["dep:itertools"]

File diff suppressed because it is too large Load Diff

949
crates/git_ui/src/git_ui.rs Normal file
View File

@@ -0,0 +1,949 @@
use editor::Editor;
use git::repository::GitFileStatus;
use gpui::*;
use ui::{prelude::*, ElevationIndex, IconButtonShape};
use ui::{Disclosure, Divider};
use workspace::item::TabContentParams;
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
pub mod git_panel;
actions!(
vcs_status,
[
Deploy,
DiscardAll,
StageAll,
DiscardSelected,
StageSelected,
UnstageSelected,
UnstageAll,
FilesChanged
]
);
#[derive(Debug, Clone)]
pub struct ChangedFile {
pub staged: bool,
pub file_path: SharedString,
pub lines_added: usize,
pub lines_removed: usize,
pub status: GitFileStatus,
}
pub struct GitLines {
pub added: usize,
pub removed: usize,
}
#[derive(IntoElement)]
pub struct ChangedFileHeader {
id: ElementId,
file: ChangedFile,
is_selected: bool,
}
impl ChangedFileHeader {
fn new(id: impl Into<ElementId>, file: ChangedFile, is_selected: bool) -> Self {
Self {
id: id.into(),
file,
is_selected,
}
}
fn icon_for_status(&self) -> impl IntoElement {
let (icon_name, color) = match self.file.status {
GitFileStatus::Added => (IconName::SquarePlus, Color::Created),
GitFileStatus::Modified => (IconName::SquareDot, Color::Modified),
GitFileStatus::Conflict => (IconName::SquareMinus, Color::Conflict),
};
Icon::new(icon_name).size(IconSize::Small).color(color)
}
}
impl RenderOnce for ChangedFileHeader {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let disclosure_id = ElementId::Name(format!("{}-file-disclosure", self.id.clone()).into());
let file_path = self.file.file_path.clone();
h_flex()
.id(self.id.clone())
.justify_between()
.w_full()
.when(!self.is_selected, |this| {
this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
})
.cursor(CursorStyle::PointingHand)
.when(self.is_selected, |this| {
this.bg(cx.theme().colors().ghost_element_active)
})
.group("")
.rounded_sm()
.px_2()
.py_1p5()
.child(
h_flex()
.gap_2()
.child(Disclosure::new(disclosure_id, false))
.child(self.icon_for_status())
.child(Label::new(file_path).size(LabelSize::Small))
.child(
h_flex()
.gap_1()
.when(self.file.lines_added > 0, |this| {
this.child(
Label::new(format!("+{}", self.file.lines_added))
.color(Color::Created)
.size(LabelSize::Small),
)
})
.when(self.file.lines_removed > 0, |this| {
this.child(
Label::new(format!("-{}", self.file.lines_removed))
.color(Color::Deleted)
.size(LabelSize::Small),
)
}),
),
)
.child(
h_flex()
.gap_2()
.child(
IconButton::new("more-menu", IconName::EllipsisVertical)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted),
)
.child(
IconButton::new("remove-file", IconName::X)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Error)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardSelected))),
)
.child(
IconButton::new("check-file", IconName::Check)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.on_click(move |_, cx| {
if self.file.staged {
cx.dispatch_action(Box::new(UnstageSelected))
} else {
cx.dispatch_action(Box::new(StageSelected))
}
}),
),
)
}
}
#[derive(IntoElement)]
pub struct GitProjectOverview {
id: ElementId,
project_status: Model<GitProjectStatus>,
}
impl GitProjectOverview {
pub fn new(id: impl Into<ElementId>, project_status: Model<GitProjectStatus>) -> Self {
Self {
id: id.into(),
project_status,
}
}
pub fn toggle_file_list(&self, cx: &mut WindowContext) {
self.project_status.update(cx, |status, cx| {
status.show_list = !status.show_list;
cx.notify();
});
}
}
impl RenderOnce for GitProjectOverview {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let status = self.project_status.read(cx);
let changed_files: SharedString =
format!("{} Changed files", status.changed_file_count()).into();
let added_label: Option<SharedString> = (status.lines_changed.added > 0)
.then(|| format!("+{}", status.lines_changed.added).into());
let removed_label: Option<SharedString> = (status.lines_changed.removed > 0)
.then(|| format!("-{}", status.lines_changed.removed).into());
let total_label: SharedString = "total lines changed".into();
h_flex()
.id(self.id.clone())
.w_full()
.bg(cx.theme().colors().elevated_surface_background)
.px_2()
.py_2p5()
.gap_2()
.child(
IconButton::new("open-sidebar", IconName::PanelLeft)
.selected(self.project_status.read(cx).show_list)
.icon_color(Color::Muted)
.on_click(move |_, cx| self.toggle_file_list(cx)),
)
.child(
h_flex()
.gap_4()
.child(Label::new(changed_files).size(LabelSize::Small))
.child(
h_flex()
.gap_1()
.when(added_label.is_some(), |this| {
this.child(
Label::new(added_label.unwrap())
.color(Color::Created)
.size(LabelSize::Small),
)
})
.when(removed_label.is_some(), |this| {
this.child(
Label::new(removed_label.unwrap())
.color(Color::Deleted)
.size(LabelSize::Small),
)
})
.child(
Label::new(total_label)
.color(Color::Muted)
.size(LabelSize::Small),
),
),
)
}
}
#[derive(IntoElement)]
pub struct GitStagingControls {
id: ElementId,
project_status: Model<GitProjectStatus>,
is_staged: bool,
is_selected: bool,
}
impl GitStagingControls {
pub fn new(
id: impl Into<ElementId>,
project_status: Model<GitProjectStatus>,
is_staged: bool,
is_selected: bool,
) -> Self {
Self {
id: id.into(),
project_status,
is_staged,
is_selected,
}
}
}
impl RenderOnce for GitStagingControls {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let status = self.project_status.read(cx);
let (staging_type, count) = if self.is_staged {
("Staged", status.staged_count())
} else {
("Unstaged", status.unstaged_count())
};
let is_expanded = if self.is_staged {
status.staged_expanded
} else {
status.unstaged_expanded
};
let label: SharedString = format!("{} Changes: {}", staging_type, count).into();
h_flex()
.id(self.id.clone())
.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
.on_click(move |_, cx| {
self.project_status.update(cx, |status, cx| {
if self.is_staged {
status.staged_expanded = !status.staged_expanded;
} else {
status.unstaged_expanded = !status.unstaged_expanded;
}
cx.notify();
})
})
.justify_between()
.w_full()
.map(|this| {
if self.is_selected {
this.bg(cx.theme().colors().ghost_element_active)
} else {
this.bg(cx.theme().colors().elevated_surface_background)
}
})
.px_3()
.py_2()
.child(
h_flex()
.gap_2()
.child(Disclosure::new(self.id.clone(), is_expanded))
.child(Label::new(label).size(LabelSize::Small)),
)
.child(h_flex().gap_2().map(|this| {
if !self.is_staged {
this.child(
Button::new(
ElementId::Name(format!("{}-discard", self.id.clone()).into()),
"Discard All",
)
.style(ButtonStyle::Filled)
.layer(ui::ElevationIndex::ModalSurface)
.size(ButtonSize::Compact)
.label_size(LabelSize::Small)
.icon(IconName::X)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.changed_file_count() == 0)
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardAll))),
)
.child(
Button::new(
ElementId::Name(format!("{}-stage", self.id.clone()).into()),
"Stage All",
)
.style(ButtonStyle::Filled)
.size(ButtonSize::Compact)
.label_size(LabelSize::Small)
.layer(ui::ElevationIndex::ModalSurface)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.no_unstaged())
.on_click(move |_, cx| cx.dispatch_action(Box::new(StageAll))),
)
} else {
this.child(
Button::new(
ElementId::Name(format!("{}-unstage", self.id.clone()).into()),
"Unstage All",
)
.layer(ui::ElevationIndex::ModalSurface)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.no_staged())
.on_click(move |_, cx| cx.dispatch_action(Box::new(UnstageAll))),
)
}
}))
}
}
pub struct GitProjectStatus {
unstaged_files: Vec<ChangedFile>,
staged_files: Vec<ChangedFile>,
lines_changed: GitLines,
staged_expanded: bool,
unstaged_expanded: bool,
show_list: bool,
selected_index: usize,
}
impl GitProjectStatus {
fn new(changed_files: Vec<ChangedFile>) -> Self {
let (unstaged_files, staged_files): (Vec<_>, Vec<_>) =
changed_files.into_iter().partition(|f| !f.staged);
let lines_changed = GitLines {
added: unstaged_files
.iter()
.chain(staged_files.iter())
.map(|f| f.lines_added)
.sum(),
removed: unstaged_files
.iter()
.chain(staged_files.iter())
.map(|f| f.lines_removed)
.sum(),
};
Self {
unstaged_files,
staged_files,
lines_changed,
staged_expanded: true,
unstaged_expanded: true,
show_list: false,
selected_index: 0,
}
}
fn changed_file_count(&self) -> usize {
self.unstaged_files.len() + self.staged_files.len()
}
fn unstaged_count(&self) -> usize {
self.unstaged_files.len()
}
fn staged_count(&self) -> usize {
self.staged_files.len()
}
fn total_item_count(&self) -> usize {
self.changed_file_count() + 2 // +2 for the two controls
}
fn no_unstaged(&self) -> bool {
self.unstaged_files.is_empty()
}
fn all_unstaged(&self) -> bool {
self.staged_files.is_empty()
}
fn no_staged(&self) -> bool {
self.staged_files.is_empty()
}
fn all_staged(&self) -> bool {
self.unstaged_files.is_empty()
}
fn update_lines_changed(&mut self) {
self.lines_changed = GitLines {
added: self
.unstaged_files
.iter()
.chain(self.staged_files.iter())
.map(|f| f.lines_added)
.sum(),
removed: self
.unstaged_files
.iter()
.chain(self.staged_files.iter())
.map(|f| f.lines_removed)
.sum(),
};
}
fn discard_all(&mut self) {
self.unstaged_files.clear();
self.staged_files.clear();
self.update_lines_changed();
}
fn stage_all(&mut self) {
self.staged_files.extend(self.unstaged_files.drain(..));
self.update_lines_changed();
}
fn unstage_all(&mut self) {
self.unstaged_files.extend(self.staged_files.drain(..));
self.update_lines_changed();
}
fn discard_selected(&mut self) {
let total_len = self.unstaged_files.len() + self.staged_files.len();
if self.selected_index > 0 && self.selected_index <= total_len {
if self.selected_index <= self.unstaged_files.len() {
self.unstaged_files.remove(self.selected_index - 1);
} else {
self.staged_files
.remove(self.selected_index - 1 - self.unstaged_files.len());
}
self.update_lines_changed();
}
}
fn stage_selected(&mut self) {
if self.selected_index > 0 && self.selected_index <= self.unstaged_files.len() {
let file = self.unstaged_files.remove(self.selected_index - 1);
self.staged_files.push(file);
self.update_lines_changed();
}
}
fn unstage_selected(&mut self) {
let unstaged_len = self.unstaged_files.len();
if self.selected_index > unstaged_len && self.selected_index <= self.total_item_count() - 2
{
let file = self
.staged_files
.remove(self.selected_index - 1 - unstaged_len);
self.unstaged_files.push(file);
self.update_lines_changed();
}
}
}
#[derive(Clone)]
pub struct ProjectStatusTab {
id: ElementId,
focus_handle: FocusHandle,
status: Model<GitProjectStatus>,
list_state: ListState,
}
impl ProjectStatusTab {
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> Self {
let changed_files = static_changed_files();
let status = cx.new_model(|_| GitProjectStatus::new(changed_files));
let status_clone = status.clone();
let list_state = ListState::new(
status.read(cx).total_item_count(),
gpui::ListAlignment::Top,
px(10.),
move |ix, cx| {
let status = status_clone.read(cx);
let is_selected = status.selected_index == ix;
if ix == 0 {
GitStagingControls::new(
"unstaged-controls",
status_clone.clone(),
false,
is_selected,
)
.into_any_element()
} else if ix == status.total_item_count() - 1 {
GitStagingControls::new(
"staged-controls",
status_clone.clone(),
true,
is_selected,
)
.into_any_element()
} else {
let file_ix = ix - 1;
let file = if file_ix < status.unstaged_count() {
status.unstaged_files.get(file_ix)
} else {
status.staged_files.get(file_ix - status.unstaged_count())
};
file.map(|file| {
ChangedFileHeader::new(
ElementId::Name(format!("file-{}", file_ix).into()),
file.clone(),
is_selected,
)
.into_any_element()
})
.unwrap_or_else(|| div().into_any_element())
}
},
);
Self {
id: id.into(),
focus_handle: cx.focus_handle(),
status,
list_state,
}
}
fn recreate_list_state(&mut self, cx: &mut ViewContext<Self>) {
let status = self.status.read(cx);
let status_clone = self.status.clone();
self.list_state = ListState::new(
status.total_item_count(),
gpui::ListAlignment::Top,
px(10.),
move |ix, cx| {
let is_selected = status_clone.read(cx).selected_index == ix;
if ix == 0 {
GitStagingControls::new(
"unstaged-controls",
status_clone.clone(),
false,
is_selected,
)
.into_any_element()
} else if ix == status_clone.read(cx).total_item_count() - 1 {
GitStagingControls::new(
"staged-controls",
status_clone.clone(),
true,
is_selected,
)
.into_any_element()
} else {
let file_ix = ix - 1;
let status = status_clone.read(cx);
let file = if file_ix < status.unstaged_count() {
status.unstaged_files.get(file_ix)
} else {
status.staged_files.get(file_ix - status.unstaged_count())
};
file.map(|file| {
ChangedFileHeader::new(
ElementId::Name(format!("file-{}", file_ix).into()),
file.clone(),
is_selected,
)
.into_any_element()
})
.unwrap_or_else(|| div().into_any_element())
}
},
);
}
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectStatusTab>(cx) {
workspace.activate_item(&existing, true, true, cx);
} else {
let status_tab = cx.new_view(|cx| Self::new("project-status-tab", cx));
workspace.add_item_to_active_pane(Box::new(status_tab), None, true, cx);
}
}
fn discard_all(&mut self, _: &DiscardAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.discard_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.stage_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.unstage_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn discard_selected(&mut self, _: &DiscardSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.discard_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn stage_selected(&mut self, _: &StageSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.stage_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn unstage_selected(&mut self, _: &UnstageSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.unstage_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn selected_index(&self, cx: &WindowContext) -> usize {
self.status.read(cx).selected_index
}
pub fn set_selected_index(
&mut self,
index: usize,
jump_to_index: bool,
cx: &mut ViewContext<Self>,
) {
self.status.update(cx, |status, _| {
status.selected_index = index.min(status.total_item_count() - 1);
});
if jump_to_index {
self.jump_to_cell(index, cx);
}
}
pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
let current_index = self.status.read(cx).selected_index;
let total_count = self.status.read(cx).total_item_count();
let new_index = (current_index + 1).min(total_count - 1);
self.set_selected_index(new_index, true, cx);
cx.notify();
}
pub fn select_previous(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
let current_index = self.status.read(cx).selected_index;
let new_index = current_index.saturating_sub(1);
self.set_selected_index(new_index, true, cx);
cx.notify();
}
pub fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.set_selected_index(0, true, cx);
cx.notify();
}
pub fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
let total_count = self.status.read(cx).total_item_count();
self.set_selected_index(total_count - 1, true, cx);
cx.notify();
}
fn jump_to_cell(&mut self, index: usize, _cx: &mut ViewContext<Self>) {
self.list_state.scroll_to_reveal_item(index);
}
}
impl Render for ProjectStatusTab {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let project_status = self.status.read(cx);
h_flex()
.id(self.id.clone())
.key_context("vcs_status")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::discard_all))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::discard_selected))
.on_action(cx.listener(Self::stage_selected))
.on_action(cx.listener(Self::unstage_selected))
.on_action(cx.listener(|this, &FilesChanged, cx| this.recreate_list_state(cx)))
.flex_1()
.size_full()
.overflow_hidden()
.when(project_status.show_list, |this| {
this.child(
v_flex()
.bg(ElevationIndex::Surface.bg(cx))
.border_r_1()
.border_color(cx.theme().colors().border)
.w(px(280.))
.flex_none()
.h_full()
.child("sidebar"),
)
})
.child(
v_flex()
.h_full()
.flex_1()
.overflow_hidden()
.bg(ElevationIndex::Surface.bg(cx))
.child(GitProjectOverview::new(
"project-overview",
self.status.clone(),
))
.child(Divider::horizontal_dashed())
.child(list(self.list_state.clone()).size_full()),
)
}
}
impl EventEmitter<()> for ProjectStatusTab {}
impl FocusableView for ProjectStatusTab {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl workspace::Item for ProjectStatusTab {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement {
Label::new("Project Status").into_any_element()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn is_singleton(&self, _cx: &AppContext) -> bool {
true
}
}
pub struct GitStatusIndicator {
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_status: Option<GitProjectStatus>,
_observe_active_editor: Option<Subscription>,
}
impl Render for GitStatusIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex().h(rems(1.375)).gap_2().child(
IconButton::new("git-status-indicator", IconName::GitBranch).on_click(cx.listener(
|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectStatusTab::deploy(workspace, &Default::default(), cx)
})
}
},
)),
)
}
}
impl GitStatusIndicator {
pub fn new(workspace: &Workspace, _: &mut ViewContext<Self>) -> Self {
Self {
active_editor: None,
workspace: workspace.weak_handle(),
current_status: None,
_observe_active_editor: None,
}
}
}
impl EventEmitter<ToolbarItemEvent> for GitStatusIndicator {}
impl StatusItemView for GitStatusIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_editor = Some(editor.downgrade());
} else {
self.active_editor = None;
self.current_status = None;
self._observe_active_editor = None;
}
cx.notify();
}
}
fn static_changed_files() -> Vec<ChangedFile> {
vec![
ChangedFile {
staged: false,
file_path: "path/to/changed_file1".into(),
lines_added: 10,
lines_removed: 5,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file2".into(),
lines_added: 8,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file3".into(),
lines_added: 15,
lines_removed: 20,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file4".into(),
lines_added: 5,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file5".into(),
lines_added: 12,
lines_removed: 7,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file6".into(),
lines_added: 0,
lines_removed: 12,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file7".into(),
lines_added: 7,
lines_removed: 3,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file8".into(),
lines_added: 2,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file9".into(),
lines_added: 18,
lines_removed: 15,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file10".into(),
lines_added: 22,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file11".into(),
lines_added: 5,
lines_removed: 5,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file12".into(),
lines_added: 7,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file13".into(),
lines_added: 3,
lines_removed: 11,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file14".into(),
lines_added: 30,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file15".into(),
lines_added: 12,
lines_removed: 22,
status: GitFileStatus::Modified,
},
]
}

View File

@@ -40,7 +40,6 @@ wayland = [
"filedescriptor",
"xkbcommon",
"open",
"scap",
]
x11 = [
"blade-graphics",
@@ -57,7 +56,6 @@ x11 = [
"x11-clipboard",
"filedescriptor",
"open",
"scap",
]
@@ -162,7 +160,6 @@ font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7"
calloop = { version = "0.13.0" }
filedescriptor = { version = "0.8.2", optional = true }
open = { version = "5.2.0", optional = true }
scap = { workspace = true, optional = true }
# Wayland
calloop-wayland-source = { version = "0.3.0", optional = true }
@@ -184,7 +181,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [
# X11
as-raw-xcb-connection = { version = "1", optional = true }
x11rb = { version = "0.13.0", features = [
x11rb = { version = "0.13.1", features = [
"allow-unsafe-code",
"xkb",
"randr",
@@ -201,7 +198,7 @@ xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf
"x11rb-xcb",
"x11rb-client",
], optional = true }
x11-clipboard = { version = "0.9.2", optional = true }
x11-clipboard = { version = "0.9.3", optional = true }
[target.'cfg(windows)'.dependencies]
blade-util.workspace = true

View File

@@ -8,7 +8,8 @@ use anyhow::{anyhow, Result};
use futures::{AsyncReadExt, Future};
use image::{
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
codecs::{gif::GifDecoder, webp::WebPDecoder},
AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
};
use smallvec::SmallVec;
use std::{
@@ -542,6 +543,34 @@ impl Asset for ImageAssetLoader {
frames
}
ImageFormat::WebP => {
let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
if decoder.has_animation() {
let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
} else {
let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
}
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();

View File

@@ -1,6 +1,10 @@
use crate::{AppContext, PlatformDispatcher};
use async_task::Runnable;
use futures::channel::mpsc;
use smol::prelude::*;
use std::mem::ManuallyDrop;
use std::panic::Location;
use std::thread::{self, ThreadId};
use std::{
fmt::Debug,
marker::PhantomData,
@@ -328,6 +332,9 @@ impl BackgroundExecutor {
/// Depending on other concurrent tasks the elapsed duration may be longer
/// than requested.
pub fn timer(&self, duration: Duration) -> Task<()> {
if duration.is_zero() {
return Task::ready(());
}
let (runnable, task) = async_task::spawn(async move {}, {
let dispatcher = self.dispatcher.clone();
move |runnable| dispatcher.dispatch_after(duration, runnable)
@@ -437,16 +444,19 @@ impl ForegroundExecutor {
}
/// Enqueues the given Task to run on the main thread at some point in the future.
#[track_caller]
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
where
R: 'static,
{
let dispatcher = self.dispatcher.clone();
#[track_caller]
fn inner<R: 'static>(
dispatcher: Arc<dyn PlatformDispatcher>,
future: AnyLocalFuture<R>,
) -> Task<R> {
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
let (runnable, task) = spawn_local_with_source_location(future, move |runnable| {
dispatcher.dispatch_on_main_thread(runnable)
});
runnable.schedule();
@@ -456,6 +466,71 @@ impl ForegroundExecutor {
}
}
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
///
/// Copy-modified from:
/// https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405
#[track_caller]
fn spawn_local_with_source_location<Fut, S>(
future: Fut,
schedule: S,
) -> (Runnable<()>, async_task::Task<Fut::Output, ()>)
where
Fut: Future + 'static,
Fut::Output: 'static,
S: async_task::Schedule<()> + Send + Sync + 'static,
{
#[inline]
fn thread_id() -> ThreadId {
std::thread_local! {
static ID: ThreadId = thread::current().id();
}
ID.try_with(|id| *id)
.unwrap_or_else(|_| thread::current().id())
}
struct Checked<F> {
id: ThreadId,
inner: ManuallyDrop<F>,
location: &'static Location<'static>,
}
impl<F> Drop for Checked<F> {
fn drop(&mut self) {
assert!(
self.id == thread_id(),
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl<F: Future> Future for Checked<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
assert!(
self.id == thread_id(),
"local task polled by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
}
}
// Wrap the future into one that checks which thread it's on.
let future = Checked {
id: thread_id(),
inner: ManuallyDrop::new(future),
location: Location::caller(),
};
unsafe { async_task::spawn_unchecked(future, schedule) }
}
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
pub struct Scope<'a> {
executor: BackgroundExecutor,

View File

@@ -16,7 +16,7 @@ use std::{
use crate::{AppContext, DisplayId};
/// An axis along which a measurement can be made.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Axis {
/// The y axis, or up and down
Vertical,
@@ -58,7 +58,21 @@ pub trait Along {
/// let point = Point { x: 10, y: 20 };
/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 }
/// ```
#[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)]
#[derive(
Refineable,
Default,
Add,
AddAssign,
Sub,
SubAssign,
Copy,
Debug,
PartialEq,
Eq,
Serialize,
Deserialize,
Hash,
)]
#[refineable(Debug)]
#[repr(C)]
pub struct Point<T: Default + Clone + Debug> {
@@ -694,7 +708,7 @@ impl Size<Length> {
/// assert_eq!(bounds.origin, origin);
/// assert_eq!(bounds.size, size);
/// ```
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
#[refineable(Debug)]
#[repr(C)]
pub struct Bounds<T: Clone + Default + Debug> {

View File

@@ -237,20 +237,20 @@ pub trait PlatformDisplay: Send + Sync + Debug {
}
/// A source of on-screen video content that can be captured.
pub trait ScreenCaptureSource: Send {
pub trait ScreenCaptureSource {
/// Returns the video resolution of this source.
fn resolution(&self) -> Result<Size<DevicePixels>>;
fn resolution(&self) -> Result<Size<Pixels>>;
/// Start capture video from this source, invoking the given callback
/// with each frame.
fn stream(
&self,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
frame_callback: Box<dyn Fn(ScreenCaptureFrame)>,
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
}
/// A video stream captured from a screen.
pub trait ScreenCaptureStream: Send {}
pub trait ScreenCaptureStream {}
/// A frame of video captured from a screen.
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);

View File

@@ -431,13 +431,25 @@ impl BladeRenderer {
}
pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
self.update_drawable_size_impl(size, false);
}
/// Like `update_drawable_size` but skips the check that the size has changed. This is useful in
/// cases like restoring a window from minimization where the size is the same but the
/// renderer's swap chain needs to be recreated.
#[cfg_attr(any(target_os = "macos", target_os = "linux"), allow(dead_code))]
pub fn update_drawable_size_even_if_unchanged(&mut self, size: Size<DevicePixels>) {
self.update_drawable_size_impl(size, true);
}
fn update_drawable_size_impl(&mut self, size: Size<DevicePixels>, always_resize: bool) {
let gpu_size = gpu::Extent {
width: size.width.0 as u32,
height: size.height.0 as u32,
depth: 1,
};
if gpu_size != self.surface_config.size {
if always_resize || gpu_size != self.surface_config.size {
self.wait_for_gpu();
self.surface_config.size = gpu_size;
self.gpu.resize(self.surface_config);

View File

@@ -21,4 +21,4 @@ pub(crate) use wayland::*;
#[cfg(feature = "x11")]
pub(crate) use x11::*;
pub type PlatformScreenCaptureFrame = platform::ScapFrame;
pub(crate) type PlatformScreenCaptureFrame = ();

View File

@@ -3,14 +3,11 @@ use std::rc::Rc;
use calloop::{EventLoop, LoopHandle};
use futures::channel::oneshot;
use util::ResultExt;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, ScreenCaptureSource, WindowParams,
};
use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams};
pub struct HeadlessClientState {
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
@@ -62,17 +59,6 @@ impl LinuxClient for HeadlessClient {
None
}
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
let (mut tx, rx) = oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"headless client does not support screen capture."
)))
.ok();
rx
}
fn active_window(&self) -> Option<AnyWindowHandle> {
None
}

View File

@@ -8,9 +8,8 @@ use std::fs::File;
use std::io::Read;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
use std::panic::Location;
use std::panic::{AssertUnwindSafe, Location};
use std::rc::Weak;
use std::sync::atomic::AtomicBool;
use std::{
path::{Path, PathBuf},
process::Command,
@@ -19,12 +18,12 @@ use std::{
time::Duration,
};
use anyhow::anyhow;
use anyhow::{anyhow, Context as _};
use async_task::Runnable;
use calloop::channel::Channel;
use calloop::{EventLoop, LoopHandle, LoopSignal};
use flume::{Receiver, Sender};
use futures::channel::oneshot;
use futures::{channel::oneshot, future::FutureExt};
use parking_lot::Mutex;
use util::ResultExt;
@@ -33,12 +32,11 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::platform::NoopTextSystem;
use crate::{
px, size, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle,
DevicePixels, DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu,
MenuItem, Modifiers, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
PlatformInputHandler, PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result,
ScreenCaptureSource, ScreenCaptureStream, SemanticVersion, SharedString, Size, Task,
WindowAppearance, WindowOptions, WindowParams,
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, OwnedMenu,
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
PlatformWindow, Point, PromptLevel, Result, ScreenCaptureSource, SemanticVersion, SharedString,
Size, Task, WindowAppearance, WindowOptions, WindowParams,
};
pub(crate) const SCROLL_LINES: f32 = 3.0;
@@ -58,9 +56,6 @@ pub trait LinuxClient {
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
fn open_window(
&self,
@@ -250,7 +245,9 @@ impl<P: LinuxClient + 'static> Platform for P {
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
self.screen_capture_sources()
let (mut tx, rx) = oneshot::channel();
tx.send(Err(anyhow!("screen capture not implemented"))).ok();
rx
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -385,14 +382,14 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn open_with_system(&self, path: &Path) {
let executor = self.background_executor().clone();
let path = path.to_owned();
executor
self.background_executor()
.spawn(async move {
let _ = std::process::Command::new("xdg-open")
.arg(path)
.spawn()
.expect("Failed to open file with xdg-open");
.context("invoking xdg-open")
.log_err();
})
.detach();
}
@@ -492,7 +489,12 @@ impl<P: LinuxClient + 'static> Platform for P {
let username = attributes
.get("username")
.ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
let secret = item.secret().await?;
// oo7 panics if the retrieved secret can't be decrypted due to
// unexpected padding.
let secret = AssertUnwindSafe(item.secret())
.catch_unwind()
.await
.map_err(|_| anyhow!("oo7 panicked while trying to read credentials"))??;
// we lose the zeroizing capabilities at this boundary,
// a current limitation GPUI's credentials api
@@ -846,31 +848,6 @@ impl Modifiers {
}
}
pub fn new_scap_capturer(target: Option<scap::Target>) -> anyhow::Result<scap::capturer::Capturer> {
Ok(scap::capturer::Capturer::build(scap::capturer::Options {
fps: 60,
show_cursor: true,
show_highlight: true,
output_type: scap::frame::FrameType::YUVFrame,
output_resolution: scap::capturer::Resolution::Captured,
crop_area: None,
target,
excluded_targets: None,
})?)
}
pub struct ScapFrame(pub scap::frame::Frame);
pub struct ScapStream(pub Arc<AtomicBool>);
impl ScreenCaptureStream for ScapStream {}
impl Drop for ScapStream {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -2,7 +2,6 @@ mod client;
mod clipboard;
mod cursor;
mod display;
mod screen_capture;
mod serial;
mod window;

View File

@@ -11,8 +11,6 @@ use calloop_wayland_source::WaylandSource;
use collections::HashMap;
use filedescriptor::Pipe;
use futures::channel::oneshot;
use http_client::Url;
use smallvec::SmallVec;
use util::ResultExt;
@@ -70,7 +68,6 @@ use crate::platform::linux::wayland::clipboard::{
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
};
use crate::platform::linux::wayland::cursor::Cursor;
use crate::platform::linux::wayland::screen_capture::wayland_screen_capture_sources;
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
@@ -81,13 +78,16 @@ use crate::platform::linux::{
};
use crate::platform::PlatformWindow;
use crate::{
point, px, size, AnyWindowHandle, Bounds, CursorStyle, DevicePixels, DisplayId, FileDropEvent,
ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScaledPixels,
ScreenCaptureSource, ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams,
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScaledPixels, ScrollDelta,
ScrollWheelEvent, TouchPhase,
};
use crate::{LinuxCommon, WindowParams};
/// Used to convert evdev scancode to xkb scancode
const MIN_KEYCODE: u32 = 8;
@@ -617,12 +617,6 @@ impl LinuxClient for WaylandClient {
None
}
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
wayland_screen_capture_sources()
}
fn open_window(
&self,
handle: AnyWindowHandle,

View File

@@ -1,124 +0,0 @@
use crate::{
new_scap_capturer, size, DevicePixels, ScapFrame, ScapStream, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, Size,
};
use anyhow::anyhow;
use futures::channel::oneshot;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
struct ScapCapturer {
stream_tx: std::sync::mpsc::Sender<(
oneshot::Sender<anyhow::Result<Box<dyn ScreenCaptureStream>>>,
Box<dyn Fn(ScreenCaptureFrame) + Send>,
)>,
size: Size<DevicePixels>,
}
impl ScreenCaptureSource for ScapCapturer {
fn resolution(&self) -> anyhow::Result<Size<DevicePixels>> {
Ok(self.size)
}
fn stream(
&self,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
) -> oneshot::Receiver<anyhow::Result<Box<dyn ScreenCaptureStream>>> {
let (tx, rx) = oneshot::channel();
self.stream_tx.send((tx, frame_callback)).ok();
rx
}
}
/// Requests that wayland prompts the user about which screen or window to capture. The receiver
/// will be filled with a capture source.
pub fn wayland_screen_capture_sources(
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
let (mut tx, result_rx) = oneshot::channel();
// Due to use of blocking APIs a dedicated thread is used.
std::thread::spawn(|| {
let (stream_tx, stream_rx) = std::sync::mpsc::channel();
let screen_capturer = util::maybe!({
// TODO: needed?
if !scap::has_permission() {
if !scap::request_permission() {
Err(anyhow!("No permissions to share screen"))?;
}
}
let mut capturer = new_scap_capturer(None)?;
// Screen capture needs to start immediately so that the size can be determined.
// In Zed the size is needed in order to initialize the LiveKit video channel.
//
// FIXME: can this be done way simpler in capture_local_video_track?
capturer.start_capture();
let size = match capturer.get_next_frame() {
Ok(frame) => get_frame_size(&frame),
Err(std::sync::mpsc::RecvError) => Err(anyhow!(
"Failed to get first frame of screenshare to get the size."
))?,
};
Ok((
capturer,
vec![Box::new(ScapCapturer { stream_tx, size }) as Box<dyn ScreenCaptureSource>],
))
});
match screen_capturer {
Err(e) => {
tx.send(Err(e)).ok();
}
Ok((mut capturer, sources)) => {
tx.send(Ok(sources)).ok();
while let Ok((tx, callback)) = stream_rx.recv() {
let cancel_stream = Arc::new(AtomicBool::new(false));
tx.send(Ok(Box::new(ScapStream(cancel_stream.clone()))))
.ok();
while cancel_stream.load(std::sync::atomic::Ordering::SeqCst) {
match capturer.get_next_frame() {
Ok(frame) => callback(ScreenCaptureFrame(ScapFrame(frame))),
Err(std::sync::mpsc::RecvError) => {
break;
}
}
}
}
capturer.stop_capture();
}
}
});
result_rx
}
fn get_frame_size(frame: &scap::frame::Frame) -> Size<DevicePixels> {
match frame {
scap::frame::Frame::YUVFrame(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::RGB(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::RGBx(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::XBGR(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::BGRx(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::BGR0(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
scap::frame::Frame::BGRA(frame) => {
size(DevicePixels(frame.width), DevicePixels(frame.height))
}
}
}

View File

@@ -1,7 +1,6 @@
mod client;
mod display;
mod event;
mod screen_capture;
mod window;
mod xim_handler;

View File

@@ -9,8 +9,8 @@ use std::time::{Duration, Instant};
use calloop::generic::{FdWrapper, Generic};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use anyhow::Context as _;
use collections::HashMap;
use futures::channel::oneshot;
use http_client::Url;
use smallvec::SmallVec;
use util::ResultExt;
@@ -40,7 +40,7 @@ use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels,
Platform, PlatformDisplay, PlatformInput, Point, RequestFrameOptions, ScaledPixels,
ScreenCaptureSource, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
};
use super::{
@@ -50,7 +50,6 @@ use super::{
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
use crate::platform::linux::x11::screen_capture::x11_screen_capture_sources;
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::{
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
@@ -1252,14 +1251,6 @@ impl LinuxClient for X11Client {
f(&mut self.0.borrow_mut().common)
}
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
let (mut tx, result_rx) = oneshot::channel();
tx.send(x11_screen_capture_sources()).ok();
result_rx
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
let state = self.0.borrow();
let setup = state.xcb_connection.setup();
@@ -1427,9 +1418,10 @@ impl LinuxClient for X11Client {
..Default::default()
},
)
.expect("failed to change window cursor")
.check()
.unwrap();
.anyhow()
.and_then(|cookie| cookie.check().anyhow())
.context("setting cursor style")
.log_err();
}
fn open_uri(&self, uri: &str) {

View File

@@ -1,82 +0,0 @@
use crate::{
new_scap_capturer, DevicePixels, ScapFrame, ScapStream, ScreenCaptureFrame,
ScreenCaptureSource, Size,
};
use anyhow::anyhow;
use futures::channel::oneshot;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
struct X11ScreenCaptureSource {
target: scap::Target,
size: Size<DevicePixels>,
}
impl ScreenCaptureSource for X11ScreenCaptureSource {
fn resolution(&self) -> anyhow::Result<Size<DevicePixels>> {
Ok(self.size)
}
fn stream(
&self,
frame_callback: Box<dyn Fn(crate::ScreenCaptureFrame) + Send>,
) -> oneshot::Receiver<anyhow::Result<Box<dyn crate::ScreenCaptureStream>>> {
let (tx, rx) = oneshot::channel();
// TODO: can clone be avoided here and elsewhere?
let target = self.target.clone();
// Due to use of blocking APIs a dedicated thread is used.
std::thread::spawn(move || {
let cancel_stream = Arc::new(AtomicBool::new(false));
let mut capturer = match new_scap_capturer(Some(target)) {
Ok(capturer) => {
tx.send(Ok(Box::new(ScapStream(cancel_stream.clone()))
as Box<dyn crate::ScreenCaptureStream>))
.ok();
capturer
}
Err(e) => {
tx.send(Err(e)).ok();
return;
}
};
while cancel_stream.load(std::sync::atomic::Ordering::SeqCst) {
match capturer.get_next_frame() {
Ok(frame) => frame_callback(ScreenCaptureFrame(ScapFrame(frame))),
Err(std::sync::mpsc::RecvError) => {
break;
}
}
}
capturer.stop_capture();
});
rx
}
}
pub fn x11_screen_capture_sources() -> anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>> {
if !scap::has_permission() {
if !scap::request_permission() {
Err(anyhow!("No permissions to share screen"))?;
}
}
// TODO(mgsloan): Handle window capture too? On Mac it's only displays.
Ok(scap::get_all_targets()
.iter()
.filter_map(|target| match target {
scap::Target::Display(display) => {
let size = Size {
width: DevicePixels(display.width as i32),
height: DevicePixels(display.height as i32),
};
Some(Box::new(X11ScreenCaptureSource {
target: target.clone(),
size,
}) as Box<dyn ScreenCaptureSource>)
}
scap::Target::Window(_) => None,
})
.collect::<Vec<_>>())
}

View File

@@ -10,7 +10,7 @@ use crate::{
PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task,
WindowAppearance, WindowParams,
};
use anyhow::anyhow;
use anyhow::{anyhow, Context as _};
use block::ConcreteBlock;
use cocoa::{
appkit::{
@@ -57,6 +57,7 @@ use std::{
sync::Arc,
};
use strum::IntoEnumIterator;
use util::ResultExt;
#[allow(non_upper_case_globals)]
const NSUTF8StringEncoding: NSUInteger = 4;
@@ -779,15 +780,16 @@ impl Platform for MacPlatform {
}
fn open_with_system(&self, path: &Path) {
let path = path.to_path_buf();
let path = path.to_owned();
self.0
.lock()
.background_executor
.spawn(async move {
std::process::Command::new("open")
let _ = std::process::Command::new("open")
.arg(path)
.spawn()
.expect("Failed to open file");
.context("invoking open command")
.log_err();
})
.detach();
}

View File

@@ -43,7 +43,7 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn resolution(&self) -> Result<Size<DevicePixels>> {
fn resolution(&self) -> Result<Size<Pixels>> {
unsafe {
let width: i64 = msg_send![self.sc_display, width];
let height: i64 = msg_send![self.sc_display, height];

View File

@@ -152,10 +152,6 @@ unsafe fn build_classes() {
sel!(flagsChanged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(cancelOperation:),
cancel_operation as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(makeBackingLayer),
@@ -1455,29 +1451,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
}
}
// Allows us to receive `cmd-.` (the shortcut for closing a dialog)
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
let keystroke = Keystroke {
modifiers: Default::default(),
key: ".".into(),
key_char: None,
};
let event = PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
});
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(event);
window_state.lock().event_callback = Some(callback);
}
}
extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = &mut *window_state.lock();

View File

@@ -1,5 +1,5 @@
use crate::{
size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
px, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource,
ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams,
};
@@ -46,13 +46,13 @@ pub struct TestScreenCaptureSource {}
pub struct TestScreenCaptureStream {}
impl ScreenCaptureSource for TestScreenCaptureSource {
fn resolution(&self) -> Result<crate::Size<crate::DevicePixels>> {
Ok(size(crate::DevicePixels(1), crate::DevicePixels(1)))
fn resolution(&self) -> Result<crate::Size<crate::Pixels>> {
Ok(size(px(1.), px(1.)))
}
fn stream(
&self,
_frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
_frame_callback: Box<dyn Fn(ScreenCaptureFrame)>,
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
let (mut tx, rx) = oneshot::channel();
let stream = TestScreenCaptureStream {};

View File

@@ -21,3 +21,5 @@ pub(crate) use window::*;
pub(crate) use wrapper::*;
pub(crate) use windows::Win32::Foundation::HWND;
pub(crate) type PlatformScreenCaptureFrame = ();

View File

@@ -33,7 +33,7 @@ pub(crate) fn handle_msg(
WM_ACTIVATE => handle_activate_msg(handle, wparam, state_ptr),
WM_CREATE => handle_create_msg(handle, state_ptr),
WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
WM_SIZE => handle_size_msg(lparam, state_ptr),
WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle),
WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle),
WM_TIMER => handle_timer_msg(handle, wparam, state_ptr),
@@ -136,13 +136,32 @@ fn handle_move_msg(
Some(0)
}
fn handle_size_msg(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
fn handle_size_msg(
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
// Don't resize the renderer when the window is minimized, but record that it was minimized so
// that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`.
if wparam.0 == SIZE_MINIMIZED as usize {
lock.is_minimized = Some(true);
return Some(0);
}
let may_have_been_minimized = lock.is_minimized.unwrap_or(true);
lock.is_minimized = Some(false);
let width = lparam.loword().max(1) as i32;
let height = lparam.hiword().max(1) as i32;
let mut lock = state_ptr.state.borrow_mut();
let new_size = size(DevicePixels(width), DevicePixels(height));
let scale_factor = lock.scale_factor;
lock.renderer.update_drawable_size(new_size);
if may_have_been_minimized {
lock.renderer
.update_drawable_size_even_if_unchanged(new_size);
} else {
lock.renderer.update_drawable_size(new_size);
}
let new_size = new_size.to_pixels(scale_factor);
lock.logical_size = new_size;
if let Some(mut callback) = lock.callbacks.resize.take() {

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