Compare commits

..

92 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
104 changed files with 7114 additions and 4808 deletions

110
Cargo.lock generated
View File

@@ -2475,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"
@@ -2668,7 +2625,6 @@ dependencies = [
"call",
"channel",
"chrono",
"clickhouse",
"client",
"clock",
"collab_ui",
@@ -5177,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"
@@ -6171,6 +6146,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"db",
"editor",
"file_icons",
"gpui",
"project",
@@ -7335,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"
@@ -11033,7 +10990,7 @@ checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.29.1",
"serde_derive_internals",
"syn 2.0.87",
]
@@ -11170,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"
@@ -11329,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"
@@ -14015,6 +13949,7 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"log",
"rand 0.8.5",
"regex",
@@ -16069,6 +16004,7 @@ dependencies = [
"futures 0.3.31",
"git",
"git_hosting_providers",
"git_ui",
"go_to_line",
"gpui",
"http_client",

View File

@@ -142,6 +142,7 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -227,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" }
@@ -360,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"

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

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

@@ -66,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",
@@ -229,7 +230,7 @@
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant2::Chat"
"enter": "assistant2::Chat"
}
},
{
@@ -788,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

@@ -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,9 +561,11 @@
// 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.
/// Diagnostics are only shown when file icons are also active.

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,6 +18,7 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]

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

View File

@@ -214,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

@@ -102,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
@@ -109,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,
),
},
)
@@ -119,8 +122,7 @@ 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?
@@ -128,7 +130,7 @@ async fn update_billing_preferences(
SnowflakeRow::new(
"Spend Limit Updated",
Some(user.metrics_id),
user.metrics_id,
user.admin,
None,
json!({

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>,
@@ -236,10 +231,6 @@ impl Config {
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
@@ -289,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,
}
@@ -343,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 {
@@ -429,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;
@@ -40,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;
@@ -52,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)>>,
}
@@ -89,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,
};
@@ -630,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),
@@ -744,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(),
@@ -763,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

@@ -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,
@@ -549,10 +548,6 @@ impl TestServer {
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,

View File

@@ -138,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);
}
}
_ => {}
@@ -363,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()
@@ -657,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 {

View File

@@ -809,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,
@@ -818,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();
}
@@ -842,10 +844,25 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
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

@@ -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;
}
}));
}
}

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,8 +17,8 @@ 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, InlineCompletion, JumpData, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
@@ -49,7 +50,7 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot,
MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@@ -1680,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,
..
@@ -1695,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
@@ -1717,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,
@@ -1755,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()
@@ -2833,8 +2842,7 @@ impl EditorElement {
return None;
}
let (text, highlights) =
inline_completion_popover_text(edit_start, editor_snapshot, edits, cx);
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);
@@ -4299,11 +4307,17 @@ impl EditorElement {
}
fn inline_completion_popover_text(
edit_start: DisplayPoint,
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();
@@ -4328,6 +4342,22 @@ fn inline_completion_popover_text(
},
));
}
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)
}
@@ -7096,13 +7126,11 @@ mod tests {
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 edit_start = DisplayPoint::new(DisplayRow(0), 6);
let edits = vec![(edit_range, " beautiful".to_string())];
let (text, highlights) =
inline_completion_popover_text(edit_start, &snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Hello, beautiful");
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..16);
assert_eq!(
@@ -7124,17 +7152,15 @@ mod tests {
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_start = DisplayPoint::new(DisplayRow(0), 0);
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(edit_start, &snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "That");
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 0..4);
assert_eq!(
@@ -7156,7 +7182,6 @@ mod tests {
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_start = DisplayPoint::new(DisplayRow(0), 0);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
@@ -7165,15 +7190,14 @@ mod tests {
),
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 13)),
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
" and universe".into(),
),
];
let (text, highlights) =
inline_completion_popover_text(edit_start, &snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Greetings, world and universe");
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);
@@ -7203,7 +7227,6 @@ mod tests {
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_start = DisplayPoint::new(DisplayRow(1), 0);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
@@ -7222,10 +7245,9 @@ mod tests {
),
];
let (text, highlights) =
inline_completion_popover_text(edit_start, &snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Second modified\nNew third line\nFourth updated");
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"

View File

@@ -18,15 +18,9 @@ use gpui::{
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
};
use language::{Buffer, BufferRow};
use multi_buffer::{
Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferPoint,
};
use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer};
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
use rand::{
distributions::{DistString, Standard},
prelude::*,
};
use text::{Edit, OffsetRangeExt, ToPoint};
use text::{OffsetRangeExt, ToPoint};
use theme::ActiveTheme;
use ui::{
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
@@ -249,7 +243,7 @@ impl ProjectDiffEditor {
.map_err(|_| anyhow!("Unexpected non-buffer"))
})
.with_context(|| {
format!("loading {} for git diff", entry_path.path.display())
format!("loading {:?} for git diff", entry_path.path)
})
.log_err()
else {
@@ -319,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();
}),
@@ -337,7 +331,6 @@ impl ProjectDiffEditor {
new_entry_order: Vec<(ProjectPath, ProjectEntryId)>,
cx: &mut ViewContext<ProjectDiffEditor>,
) {
println!("update_excerpts.................");
if let Some(current_order) = self.entry_order.get(&worktree_id) {
let current_entries = self.buffer_changes.entry(worktree_id).or_default();
let mut new_order_entries = new_entry_order.iter().fuse().peekable();
@@ -1106,11 +1099,8 @@ impl Render for ProjectDiffEditor {
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use futures::{prelude::*, stream::FuturesUnordered};
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use project::buffer_store::BufferChangeSet;
use rand::distributions::Alphanumeric;
use serde_json::json;
use settings::SettingsStore;
use std::{
@@ -1118,275 +1108,127 @@ mod tests {
path::{Path, PathBuf},
};
use crate::hunks_for_ranges;
use super::*;
#[gpui::test(iterations = 100)]
async fn random_edits(cx: &mut TestAppContext, mut rng: StdRng) {
// TODO switch to RandomCharIter from util or the random_edits thing
fn line(rng: &mut StdRng) -> String {
let len = rng.gen_range(0..20);
let mut s = Alphanumeric.sample_string(rng, len);
s.push('\n');
s
}
// TODO finish
// #[gpui::test]
// async fn randomized_tests(cx: &mut TestAppContext) {
// // Create a new project (how?? temp fs?),
// let fs = FakeFs::new(cx.executor());
// let project = Project::test(fs, [], cx).await;
fn original_file(rng: &mut StdRng) -> String {
let line_count = rng.gen_range(0..10);
(0..line_count).map(|_| line(rng)).collect()
}
// // create random files with random content
fn edit_file(rng: &mut StdRng, old: &str) -> Vec<(Range<usize>, Range<usize>, String)> {
let mut old_lines = old.lines().collect::<Vec<_>>().into_iter();
let mut edits = Vec::new();
let u = rng.gen_range(0..=old_lines.len());
let mut old_offset = old_lines
.by_ref()
.take(u)
.map(|line| line.len() + 1)
.sum::<usize>();
let mut new_offset = old_offset;
while old_lines.len() > 0 {
let d = rng.gen_range(0..=old_lines.len());
let advance = old_lines
.by_ref()
.take(d)
.map(|line| line.len() + 1)
.sum::<usize>();
let d_range = old_offset..old_offset + advance;
old_offset += advance;
let a_min = if d == 0 { 1 } else { 0 };
let a = rng.gen_range(a_min..=5);
let piece = (0..a).map(|_| line(rng)).collect::<String>();
let a_range = new_offset..new_offset + piece.len();
new_offset += piece.len();
edits.push((d_range, a_range, piece));
if old_lines.len() > 0 {
let u = rng.gen_range(1..=old_lines.len());
let advance = old_lines
.by_ref()
.take(u)
.map(|line| line.len() + 1)
.sum::<usize>();
old_offset += advance;
new_offset += advance;
}
}
edits
}
// // Commit it into git somehow (technically can do with "real" fs in a temp dir)
// //
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
// }
#[gpui::test(iterations = 30)]
async fn simple_edit_test(cx: &mut TestAppContext) {
cx.executor().allow_parking();
init_test(cx);
let rng = &mut rng;
let originals = HashMap::from_iter([
("file0", original_file(rng)),
// ("file1", original_file(rng)),
// ("file2", original_file(rng)),
]);
let fs = fs::FakeFs::new(cx.executor().clone());
let mut files = json!(originals);
files
.as_object_mut()
.unwrap()
.insert(".git".to_owned(), json!({}));
fs.insert_tree("/project", files).await;
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
fs.insert_tree(
"/root",
json!({
".git": {},
"file_a": "This is file_a",
"file_b": "This is file_b",
}),
)
.await;
let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let (file_editors, project_diff_editor) = workspace
let file_a_editor = workspace
.update(cx, |workspace, cx| {
let file_editors = originals
.keys()
.map(|name| {
workspace.open_abs_path(
PathBuf::from(format!("/project/{}", name)),
true,
cx,
)
})
.collect::<Vec<_>>();
let file_a_editor =
workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
let project_diff_editor = workspace
file_a_editor
})
.unwrap()
.await
.expect("did not open an item at all")
.downcast::<Editor>()
.expect("did not open an editor for file_a");
let project_diff_editor = workspace
.update(cx, |workspace, cx| {
workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ProjectDiffEditor>())
.expect("Didn't open project diff editor");
(file_editors, project_diff_editor)
})
.unwrap();
let file_editors = file_editors
.into_iter()
.collect::<FuturesUnordered<_>>()
.map(|result| result?.downcast::<Editor>().ok_or(anyhow!("downcast")))
.try_collect::<Vec<_>>()
.await
.expect("Didn't open file editors");
.unwrap()
.expect("did not find a ProjectDiffEditor");
project_diff_editor.update(cx, |project_diff_editor, cx| {
assert!(
project_diff_editor.editor.read(cx).text(cx).is_empty(),
"Should have no diff before files are edited"
"Should have no changes after opening the diff on no git changes"
);
});
let mut all_edits = Vec::new();
for editor in &file_editors {
let (mut old_text, mut edits) = (String::new(), Vec::new());
editor
.update(cx, |editor, cx| {
old_text = dbg!(editor.text(cx));
edits = dbg!(edit_file(rng, &old_text));
editor.edit(
edits
.clone()
.into_iter()
.map(|(old, _new, content)| (old, content)),
cx,
);
editor.save(false, project.clone(), cx)
})
.await
.expect("Failed to save edits");
let buffer_id = editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
let snapshot = buffer.text_snapshot();
let id = buffer.remote_id();
let change_set =
cx.new_model(|cx| BufferChangeSet::new_with_base_text(old_text, snapshot, cx));
editor
.diff_map
.add_change_set_with_project(project.clone(), change_set, cx);
id
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
let change = "an edit after git add";
file_a_editor
.update(cx, |file_a_editor, cx| {
file_a_editor.insert(change, cx);
file_a_editor.save(false, project.clone(), cx)
})
.await
.expect("failed to save a file");
file_a_editor.update(cx, |file_a_editor, cx| {
let change_set = cx.new_model(|cx| {
BufferChangeSet::new_with_base_text(
old_text.clone(),
file_a_editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.text_snapshot(),
cx,
)
});
all_edits.extend(edits.into_iter().map(|(old, new, _)| (buffer_id, old, new)));
}
file_a_editor
.diff_map
.add_change_set(change_set.clone(), cx);
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.set_change_set(
file_a_editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.remote_id(),
change_set,
);
});
});
});
fs.set_status_for_repo_via_git_operation(
Path::new("/project/.git"),
&originals
.keys()
.map(|name| (Path::new(name), GitFileStatus::Modified))
.collect::<Vec<_>>(),
Path::new("/root/.git"),
&[(Path::new("file_a"), GitFileStatus::Modified)],
);
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
project_diff_editor.update(cx, |project_diff_editor, cx| {
let mut hunks: Vec<_> = project_diff_editor.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
hunks_for_ranges(
[MultiBufferPoint::zero()..snapshot.buffer_snapshot.max_point()].into_iter(),
&snapshot,
)
.into_iter()
.map(|hunk| {
let point = MultiBufferPoint::new(hunk.row_range.start.0, 0);
let buffer_snapshot = snapshot
.buffer_snapshot
.excerpt_containing(point..point)
.unwrap()
.buffer();
(
hunk.buffer_id,
hunk.diff_base_byte_range,
hunk.buffer_range.to_offset(buffer_snapshot),
)
})
.collect()
});
hunks.sort_by_key(|(buffer_id, old, _)| (*buffer_id, old.start));
all_edits.sort_by_key(|(buffer_id, old, _)| (*buffer_id, old.start));
pretty_assertions::assert_eq!(hunks, all_edits);
});
}
#[gpui::test]
async fn repro(cx: &mut TestAppContext) {
let old_text = "r4zU3hQFgVh74o\n";
let edit = (0..15, "");
let new_text = "";
cx.executor().allow_parking();
init_test(cx);
let fs = fs::FakeFs::new(cx.executor().clone());
fs.insert_tree("/project", json!({".git": {}, "file": old_text}))
.await;
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let (editor, project_diff_editor) = workspace
.update(cx, |workspace, cx| {
let editor = workspace.open_abs_path("/project/file".into(), true, cx);
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
let project_diff_editor = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ProjectDiffEditor>())
.expect("Didn't open project diff editor");
(editor, project_diff_editor)
})
.unwrap();
let editor = editor
.await
.and_then(|item| item.downcast::<Editor>().ok_or(anyhow!("downcast")))
.unwrap();
editor
.update(cx, |editor, cx| {
editor.edit([edit.clone()], cx);
editor.save(false, project.clone(), cx)
})
.await
.expect("failed to save a file");
editor.update(cx, |editor, cx| {
let change_set = cx.new_model(|cx| {
let snapshot = editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.text_snapshot();
assert_eq!(snapshot.text(), new_text);
BufferChangeSet::new_with_base_text(old_text.to_owned(), snapshot, cx)
});
editor
.diff_map
.add_change_set_with_project(project.clone(), change_set, cx);
});
fs.set_status_for_repo_via_git_operation(
Path::new("/project/.git"),
&[(Path::new("file"), GitFileStatus::Modified)],
);
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(1000));
cx.run_until_parked();
project_diff_editor.update(cx, |project_diff_editor, cx| {
let mut hunks: Vec<_> = project_diff_editor.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
hunks_for_ranges(
[MultiBufferPoint::zero()..snapshot.buffer_snapshot.max_point()].into_iter(),
&snapshot,
)
.into_iter()
.map(|hunk| {
let point = MultiBufferPoint::new(hunk.row_range.start.0, 0);
let buffer_snapshot = snapshot
.buffer_snapshot
.excerpt_containing(point..point)
.unwrap()
.buffer();
hunk.diff_base_byte_range
})
.collect()
});
pretty_assertions::assert_eq!(hunks, [edit.0]);
assert_eq!(
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
project_diff_editor.editor.read(cx).text(cx),
format!("{change}{old_text}"),
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
);
});
}

View File

@@ -9,7 +9,7 @@ use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToOffset, ToPoint,
};
use project::{buffer_store::BufferChangeSet, Project};
use project::buffer_store::BufferChangeSet;
use std::{ops::Range, sync::Arc};
use sum_tree::TreeMap;
use text::OffsetRangeExt;
@@ -18,7 +18,7 @@ use ui::{
ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
};
use util::RangeExt;
use workspace::{Item, ItemHandle};
use workspace::Item;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
@@ -80,40 +80,6 @@ impl DiffMap {
self.snapshot.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn add_change_set_with_project(
&mut self,
project: Model<Project>,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Editor>,
) {
let buffer_id = change_set.read(cx).buffer_id;
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 {
last_version: None,
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
editor
.diff_map
.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
}),
change_set: change_set.clone(),
},
);
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.set_change_set(buffer_id, change_set);
});
});
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
@@ -123,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 {
@@ -139,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> {
@@ -171,15 +137,10 @@ impl DiffMapSnapshot {
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let Some(diff) = self.0.get(&buffer_id) else {
eprintln!("boom");
dbg!(&self.0);
return None;
};
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
dbg!("some hunks");
Some(
diff.hunks_intersecting_range(buffer_range, excerpt.buffer())
.map(move |hunk| {

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

@@ -69,14 +69,12 @@ pub struct BufferDiff {
impl BufferDiff {
pub fn new(buffer: &BufferSnapshot) -> BufferDiff {
dbg!("BufferDiff::new");
BufferDiff {
tree: SumTree::new(buffer),
}
}
pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
dbg!("BufferDiff::build");
let mut tree = SumTree::new(buffer);
let buffer_text = buffer.as_rope().to_string();
@@ -86,7 +84,7 @@ impl BufferDiff {
let mut divergence = 0;
for hunk_index in 0..patch.num_hunks() {
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
tree.push(dbg!(hunk), buffer);
tree.push(hunk, buffer);
}
}
@@ -143,12 +141,11 @@ impl BufferDiff {
end_point.column = 0;
}
let hunk = DiffHunk {
Some(DiffHunk {
row_range: start_point.row..end_point.row,
diff_base_byte_range: start_base..end_base,
buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point),
};
Some(dbg!(hunk))
})
})
}
@@ -190,12 +187,11 @@ impl BufferDiff {
}
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
dbg!("BufferDiff::update");
*self = Self::build(&diff_base.to_string(), buffer).await;
}
#[cfg(any(test, feature = "test-support"))]
pub fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
#[cfg(test)]
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
self.hunks_intersecting_range(start..end, text)
@@ -229,60 +225,60 @@ impl BufferDiff {
buffer: &text::BufferSnapshot,
buffer_row_divergence: &mut i64,
) -> InternalDiffHunk {
dbg!("BufferDiff::process_patch_hunk");
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
assert!(line_item_count > 0);
let mut first_deletion_buffer_row: Option<u32> = None;
let mut buffer_row_range: Option<Range<u32>> = None;
let mut diff_base_byte_range: Option<Range<usize>> = None;
let fallback_offset = patch.line_in_hunk(hunk_index, 0).unwrap().content_offset() as usize;
let fallback_range = fallback_offset..fallback_offset;
for line_index in 0..line_item_count {
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
let kind = line.origin_value();
let content_offset = line.content_offset() as isize;
let content_len = line.content().len() as isize;
match kind {
GitDiffLineType::Addition => {
*buffer_row_divergence += 1;
let row = line.new_lineno().unwrap().saturating_sub(1);
match &mut buffer_row_range {
Some(buffer_row_range) => buffer_row_range.end = row + 1,
None => buffer_row_range = Some(row..row + 1),
}
if kind == GitDiffLineType::Addition {
*buffer_row_divergence += 1;
let row = line.new_lineno().unwrap().saturating_sub(1);
match &mut buffer_row_range {
Some(buffer_row_range) => buffer_row_range.end = row + 1,
None => buffer_row_range = Some(row..row + 1),
}
GitDiffLineType::Deletion => {
let end = content_offset + content_len;
}
match &mut diff_base_byte_range {
Some(head_byte_range) => head_byte_range.end = end as usize,
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
}
if kind == GitDiffLineType::Deletion {
let end = content_offset + content_len;
if first_deletion_buffer_row.is_none() {
let old_row = line.old_lineno().unwrap().saturating_sub(1);
let row = old_row as i64 + *buffer_row_divergence;
first_deletion_buffer_row = Some(row as u32);
}
*buffer_row_divergence -= 1;
match &mut diff_base_byte_range {
Some(head_byte_range) => head_byte_range.end = end as usize,
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
}
_ => {}
if first_deletion_buffer_row.is_none() {
let old_row = line.old_lineno().unwrap().saturating_sub(1);
let row = old_row as i64 + *buffer_row_divergence;
first_deletion_buffer_row = Some(row as u32);
}
*buffer_row_divergence -= 1;
}
}
//unwrap_or deletion without addition
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
// Pure deletion hunk without addition.
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
let row = first_deletion_buffer_row.unwrap();
row..row
});
//unwrap_or addition without deletion
let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
let start = Point::new(buffer_row_range.start, 0);
let end = Point::new(buffer_row_range.end, 0);
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
let diff_base_byte_range = diff_base_byte_range.unwrap_or(fallback_range);
InternalDiffHunk {
buffer_range,
diff_base_byte_range,

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

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

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

@@ -141,16 +141,27 @@ fn handle_size_msg(
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() {

View File

@@ -38,6 +38,7 @@ pub struct WindowsWindowState {
pub fullscreen_restore_bounds: Bounds<Pixels>,
pub border_offset: WindowBorderOffset,
pub scale_factor: f32,
pub is_minimized: Option<bool>,
pub callbacks: Callbacks,
pub input_handler: Option<PlatformInputHandler>,
@@ -92,6 +93,7 @@ impl WindowsWindowState {
size: logical_size,
};
let border_offset = WindowBorderOffset::default();
let is_minimized = None;
let renderer = windows_renderer::windows_renderer(hwnd, transparent)?;
let callbacks = Callbacks::default();
let input_handler = None;
@@ -109,6 +111,7 @@ impl WindowsWindowState {
fullscreen_restore_bounds,
border_offset,
scale_factor,
is_minimized,
callbacks,
input_handler,
system_key_handled,

View File

@@ -271,6 +271,20 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
self
}
/// Sets cursor style when hovering over an element to `nesw-resize`.
/// [Docs](https://tailwindcss.com/docs/cursor)
#visibility fn cursor_nesw_resize(mut self) -> Self {
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpRightDownLeft);
self
}
/// Sets cursor style when hovering over an element to `nwse-resize`.
/// [Docs](https://tailwindcss.com/docs/cursor)
#visibility fn cursor_nwse_resize(mut self) -> Self {
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpLeftDownRight);
self
}
/// Sets cursor style when hovering over an element to `col-resize`.
/// [Docs](https://tailwindcss.com/docs/cursor)
#visibility fn cursor_col_resize(mut self) -> Self {

View File

@@ -15,6 +15,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
db.workspace = true
editor.workspace = true
file_icons.workspace = true
gpui.workspace = true
project.workspace = true

View File

@@ -1,6 +1,7 @@
use std::path::PathBuf;
use anyhow::Context as _;
use editor::items::entry_git_aware_label_color;
use gpui::{
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
@@ -94,15 +95,29 @@ impl Item for ImageView {
}
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let path = self.image_item.read(cx).file.path();
let title = path
.file_name()
.unwrap_or_else(|| path.as_os_str())
let project_path = self.image_item.read(cx).project_path(cx);
let label_color = if ItemSettings::get_global(cx).git_status {
self.project
.read(cx)
.entry_for_path(&project_path, cx)
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
})
.unwrap_or_else(|| params.text_color())
} else {
params.text_color()
};
let title = self
.image_item
.read(cx)
.file
.file_name(cx)
.to_string_lossy()
.to_string();
Label::new(title)
.single_line()
.color(params.text_color())
.color(label_color)
.italic(params.preview)
.into_any_element()
}
@@ -146,7 +161,7 @@ impl Item for ImageView {
}
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &AppContext) -> String {
let path = image.path();
let path = image.file.file_name(cx);
if project.visible_worktrees(cx).count() <= 1 {
return path.to_string_lossy().to_string();
}

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use copilot::{Copilot, Status};
use editor::{scroll::Autoscroll, Editor};
use feature_flags::FeatureFlagAppExt;
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
@@ -199,12 +199,12 @@ impl Render for InlineCompletionButton {
}
InlineCompletionProvider::Zeta => {
if !cx.is_staff() {
if !cx.has_flag::<ZetaFeatureFlag>() {
return div();
}
div().child(
Button::new("zeta", "Zeta")
Button::new("zeta", "ζ")
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {

View File

@@ -138,6 +138,12 @@ pub struct LanguageSettings {
pub linked_edits: bool,
/// Task configuration for this language.
pub tasks: LanguageTaskConfig,
/// Whether to pop the completions menu while typing in an editor without
/// explicitly requesting it.
pub show_completions_on_input: bool,
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
pub show_completion_documentation: bool,
}
impl LanguageSettings {
@@ -382,6 +388,16 @@ pub struct LanguageSettingsContent {
///
/// Default: {}
pub tasks: Option<LanguageTaskConfig>,
/// 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 contents of the inline completion settings.
@@ -1186,6 +1202,14 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.extend_comment_on_newline,
);
merge(&mut settings.inlay_hints, src.inlay_hints);
merge(
&mut settings.show_completions_on_input,
src.show_completions_on_input,
);
merge(
&mut settings.show_completion_documentation,
src.show_completion_documentation,
);
}
/// Allows to enable/disable formatting with Prettier

View File

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

View File

@@ -2198,7 +2198,6 @@ impl BufferChangeSet {
range: Range<text::Anchor>,
buffer_snapshot: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk> {
dbg!("");
self.diff_to_buffer
.hunks_intersecting_range(range, buffer_snapshot)
}

View File

@@ -123,9 +123,17 @@ impl ProjectItem for ImageItem {
let path = path.clone();
let project = project.clone();
let ext = path
.path
let worktree_abs_path = project
.read(cx)
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.abs_path();
// Resolve the file extension from either the worktree path (if it's a single file)
// or from the project path's subpath.
let ext = worktree_abs_path
.extension()
.or_else(|| path.path.extension())
.and_then(OsStr::to_str)
.map(str::to_lowercase)
.unwrap_or_default();

View File

@@ -1,9 +1,10 @@
mod signature_help;
use crate::{
lsp_store::LspStore, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
InlayHintTooltip, Location, LocationLink, MarkupContent, ProjectTransaction, ResolveState,
lsp_store::{LocalLspStore, LspStore},
CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint,
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
LocationLink, MarkupContent, ProjectTransaction, ResolveState,
};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
@@ -348,7 +349,7 @@ impl LspCommand for PerformRename {
if let Some(edit) = message {
let (lsp_adapter, lsp_server) =
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
LspStore::deserialize_workspace_edit(
LocalLspStore::deserialize_workspace_edit(
lsp_store,
edit,
self.push_to_history,
@@ -837,7 +838,7 @@ fn language_server_for_buffer(
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store
.language_server_for_buffer(buffer.read(cx), server_id, cx)
.language_server_for_local_buffer(buffer.read(cx), server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone()))
})?
.ok_or_else(|| anyhow!("no language server found for buffer"))
@@ -2306,7 +2307,7 @@ impl LspCommand for OnTypeFormatting {
if let Some(edits) = message {
let (lsp_adapter, lsp_server) =
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
LspStore::deserialize_text_edits(
LocalLspStore::deserialize_text_edits(
lsp_store,
buffer,
edits,

File diff suppressed because it is too large Load Diff

View File

@@ -47,9 +47,9 @@ use gpui::{
use itertools::Itertools;
use language::{
language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent,
CachedLspAdapter, Capability, CodeLabel, DiagnosticEntry, Documentation, File as _, Language,
LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList,
Transaction, Unclipped,
CachedLspAdapter, Capability, CodeLabel, Documentation, File as _, Language, LanguageName,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
Unclipped,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer,
@@ -2568,31 +2568,6 @@ impl Project {
.update(cx, |store, _| store.reset_last_formatting_failure());
}
pub fn update_diagnostics(
&mut self,
language_server_id: LanguageServerId,
params: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
cx: &mut ModelContext<Self>,
) -> Result<()> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, params, disk_based_sources, cx)
})
}
pub fn update_diagnostic_entries(
&mut self,
server_id: LanguageServerId,
abs_path: PathBuf,
version: Option<i32>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut ModelContext<Project>,
) -> Result<(), anyhow::Error> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostic_entries(server_id, abs_path, version, diagnostics, cx)
})
}
pub fn reload_buffers(
&self,
buffers: HashSet<Model<Buffer>>,
@@ -3446,12 +3421,9 @@ impl Project {
}
pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
let mut summary = DiagnosticSummary::default();
for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) {
summary.error_count += path_summary.error_count;
summary.warning_count += path_summary.warning_count;
}
summary
self.lsp_store
.read(cx)
.diagnostic_summary(include_ignored, cx)
}
pub fn diagnostic_summaries<'a>(
@@ -4163,13 +4135,6 @@ impl Project {
Ok(())
}
pub fn language_servers<'a>(
&'a self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = (LanguageServerId, LanguageServerName, WorktreeId)> {
self.lsp_store.read(cx).language_servers()
}
pub fn supplementary_language_servers<'a>(
&'a self,
cx: &'a AppContext,
@@ -4185,14 +4150,14 @@ impl Project {
self.lsp_store.read(cx).language_server_for_id(id)
}
pub fn language_servers_for_buffer<'a>(
pub fn language_servers_for_local_buffer<'a>(
&'a self,
buffer: &'a Buffer,
cx: &'a AppContext,
) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
self.lsp_store
.read(cx)
.language_servers_for_buffer(buffer, cx)
.language_servers_for_local_buffer(buffer, cx)
}
pub fn buffer_store(&self) -> &Model<BufferStore> {

View File

@@ -6,8 +6,9 @@ use gpui::{AppContext, SemanticVersion, UpdateGlobal};
use http_client::Url;
use language::{
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter,
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, DiagnosticSet,
DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding,
OffsetRangeExt, Point, ToPoint,
};
use lsp::{
notification::DidRenameFiles, DiagnosticSeverity, DocumentChanges, FileOperationFilter,
@@ -987,6 +988,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let buffer_a = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -997,8 +999,8 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
project.update(cx, |project, cx| {
project
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
@@ -1015,7 +1017,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
cx,
)
.unwrap();
project
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
@@ -1086,6 +1088,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/root/dir", true, cx)
@@ -1103,8 +1106,8 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
let server_id = LanguageServerId(0);
project.update(cx, |project, cx| {
project
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
@@ -1121,7 +1124,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
cx,
)
.unwrap();
project
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
@@ -2018,9 +2021,9 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| {
project.lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_buffer_diagnostics(
&buffer,
.update_diagnostic_entries(
LanguageServerId(0),
PathBuf::from("/dir/a.rs"),
None,
vec![
DiagnosticEntry {
@@ -2078,9 +2081,10 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store.clone());
project.update(cx, |project, cx| {
project
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
LanguageServerId(0),
Path::new("/dir/a.rs").to_owned(),
@@ -2097,7 +2101,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
cx,
)
.unwrap();
project
lsp_store
.update_diagnostic_entries(
LanguageServerId(1),
Path::new("/dir/a.rs").to_owned(),
@@ -2116,7 +2120,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
.unwrap();
assert_eq!(
project.diagnostic_summary(false, cx),
lsp_store.diagnostic_summary(false, cx),
DiagnosticSummary {
error_count: 2,
warning_count: 0,
@@ -2218,7 +2222,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
let edits = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.edits_from_lsp(
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer,
vec![
// replace body of first function
@@ -2313,7 +2317,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp
// Rust-analyzer does this when performing a merge-imports code action.
let edits = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.edits_from_lsp(
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer,
[
// Replace the first use statement without editing the semicolon.
@@ -2422,7 +2426,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
// with ranges sometimes being inverted or pointing to invalid locations.
let edits = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.edits_from_lsp(
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer,
[
lsp::TextEdit {
@@ -3698,6 +3702,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
.await
@@ -3791,9 +3796,9 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
version: None,
};
project
.update(cx, |p, cx| {
p.update_diagnostics(LanguageServerId(0), message, &[], cx)
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx)
})
.unwrap();
let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());

View File

@@ -105,7 +105,6 @@ pub struct ProjectPanel {
// We keep track of the mouse down state on entries so we don't flash the UI
// in case a user clicks to open a file.
mouse_down: bool,
hovered_entries: HashSet<ProjectEntryId>,
}
#[derive(Clone, Debug)]
@@ -144,7 +143,6 @@ struct EntryDetails {
is_marked: bool,
is_editing: bool,
is_processing: bool,
is_hovered: bool,
is_cut: bool,
filename_text_color: Color,
diagnostic_severity: Option<DiagnosticSeverity>,
@@ -403,7 +401,6 @@ impl ProjectPanel {
diagnostics: Default::default(),
scroll_handle,
mouse_down: false,
hovered_entries: Default::default(),
};
this.update_visible_entries(None, cx);
@@ -2801,7 +2798,6 @@ impl ProjectPanel {
is_expanded,
is_selected: self.selection == Some(selection),
is_marked,
is_hovered: self.hovered_entries.contains(&entry.id),
is_editing: false,
is_processing: false,
is_cut: self
@@ -3157,7 +3153,6 @@ impl ProjectPanel {
let is_active = self
.selection
.map_or(false, |selection| selection.entry_id == entry_id);
let is_hovered = details.is_hovered;
let width = self.size(cx);
let file_name = details.filename.clone();
@@ -3190,14 +3185,6 @@ impl ProjectPanel {
marked_selections: selections,
};
let (bg_color, border_color) = match (is_hovered, is_marked || is_active, self.mouse_down) {
(true, _, true) => (item_colors.marked_active, item_colors.hover),
(true, false, false) => (item_colors.hover, item_colors.hover),
(true, true, false) => (item_colors.hover, item_colors.marked_active),
(false, true, _) => (item_colors.marked_active, item_colors.marked_active),
_ => (item_colors.default, item_colors.default),
};
div()
.id(entry_id.to_proto() as usize)
.when(is_local, |div| {
@@ -3275,14 +3262,6 @@ impl ProjectPanel {
cx.propagate();
}),
)
.on_hover(cx.listener(move |this, hover, cx| {
if *hover {
this.hovered_entries.insert(entry_id);
} else {
this.hovered_entries.remove(&entry_id);
}
cx.notify();
}))
.on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
if event.down.button == MouseButton::Right || event.down.first_mouse || show_editor
{
@@ -3344,13 +3323,11 @@ impl ProjectPanel {
}
}))
.cursor_pointer()
.bg(bg_color)
.border_color(border_color)
.child(
ListItem::new(entry_id.to_proto() as usize)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.selectable(false)
.selected(is_marked || is_active)
.when_some(canonical_path, |this, path| {
this.end_slot::<AnyElement>(
div()
@@ -3390,7 +3367,11 @@ impl ProjectPanel {
} else {
IconDecorationKind::Dot
},
bg_color,
if is_marked || is_active {
item_colors.marked_active
} else {
item_colors.default
},
cx,
)
.color(decoration_color.color(cx))
@@ -3511,6 +3492,19 @@ impl ProjectPanel {
.border_1()
.border_r_2()
.rounded_none()
.hover(|style| {
if is_active {
style
} else {
style.bg(item_colors.hover).border_color(item_colors.hover)
}
})
.when(is_marked || is_active, |this| {
this.when(is_marked, |this| {
this.bg(item_colors.marked_active)
.border_color(item_colors.marked_active)
})
})
.when(
!self.mouse_down && is_active && self.focus_handle.contains_focused(cx),
|this| this.border_color(item_colors.focused),

View File

@@ -73,21 +73,27 @@ pub fn init(cx: &mut AppContext) {
return;
}
let project_path = editor
.buffer()
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).project_path(cx));
let buffer = editor.buffer().read(cx).as_singleton();
let language = buffer
.as_ref()
.and_then(|buffer| buffer.read(cx).language());
let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
let editor_handle = cx.view().downgrade();
if let (Some(project_path), Some(project)) = (project_path, project) {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
.detach_and_log_err(cx);
});
if let Some(language) = language {
if language.name() == "Python".into() {
if let (Some(project_path), Some(project)) = (project_path, project) {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
.detach_and_log_err(cx);
});
}
}
}
editor

View File

@@ -173,7 +173,7 @@ impl ReplStore {
let remote_kernel_specifications = self.get_remote_kernel_specifications(cx);
cx.spawn(|this, mut cx| async move {
let all_specs = cx.background_executor().spawn(async move {
let mut all_specs = local_kernel_specifications
.await?
.into_iter()
@@ -186,10 +186,21 @@ impl ReplStore {
}
}
this.update(&mut cx, |this, cx| {
this.kernel_specifications = all_specs;
cx.notify();
})
anyhow::Ok(all_specs)
});
cx.spawn(|this, mut cx| async move {
let all_specs = all_specs.await;
if let Ok(specs) = all_specs {
this.update(&mut cx, |this, cx| {
this.kernel_specifications = specs;
cx.notify();
})
.ok();
}
anyhow::Ok(())
})
}

View File

@@ -912,10 +912,15 @@ pub fn new_terminal_pane(
let new_pane = pane.drag_split_direction().and_then(|split_direction| {
terminal_panel.update(cx, |terminal_panel, cx| {
let is_zoomed = if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
terminal_panel.active_pane.read(cx).is_zoomed()
};
let new_pane = new_terminal_pane(
workspace.clone(),
project.clone(),
terminal_panel.active_pane.read(cx).is_zoomed(),
is_zoomed,
cx,
);
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);

View File

@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
"A button allows users to take actions, and make choices, with a single tap."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Styles",

View File

@@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
@@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Unselected",

View File

@@ -3,6 +3,13 @@ use gpui::{Hsla, IntoElement};
use crate::prelude::*;
#[derive(Clone, Copy, PartialEq)]
enum DividerStyle {
Solid,
Dashed,
}
#[derive(Clone, Copy, PartialEq)]
enum DividerDirection {
Horizontal,
Vertical,
@@ -27,6 +34,7 @@ impl DividerColor {
#[derive(IntoElement)]
pub struct Divider {
style: DividerStyle,
direction: DividerDirection,
color: DividerColor,
inset: bool,
@@ -34,22 +42,17 @@ pub struct Divider {
impl RenderOnce for Divider {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
}
DividerDirection::Vertical => {
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
}
})
.bg(self.color.hsla(cx))
match self.style {
DividerStyle::Solid => self.render_solid(cx).into_any_element(),
DividerStyle::Dashed => self.render_dashed(cx).into_any_element(),
}
}
}
impl Divider {
pub fn horizontal() -> Self {
Self {
style: DividerStyle::Solid,
direction: DividerDirection::Horizontal,
color: DividerColor::default(),
inset: false,
@@ -58,6 +61,25 @@ impl Divider {
pub fn vertical() -> Self {
Self {
style: DividerStyle::Solid,
direction: DividerDirection::Vertical,
color: DividerColor::default(),
inset: false,
}
}
pub fn horizontal_dashed() -> Self {
Self {
style: DividerStyle::Dashed,
direction: DividerDirection::Horizontal,
color: DividerColor::default(),
inset: false,
}
}
pub fn vertical_dashed() -> Self {
Self {
style: DividerStyle::Dashed,
direction: DividerDirection::Vertical,
color: DividerColor::default(),
inset: false,
@@ -73,4 +95,47 @@ impl Divider {
self.color = color;
self
}
pub fn render_solid(self, cx: &WindowContext) -> impl IntoElement {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
}
DividerDirection::Vertical => {
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
}
})
.bg(self.color.hsla(cx))
}
pub fn render_dashed(self, cx: &WindowContext) -> impl IntoElement {
let segment_count = 128;
let segment_count_f = segment_count as f32;
let segment_min_w = 6.;
let base = match self.direction {
DividerDirection::Horizontal => h_flex(),
DividerDirection::Vertical => v_flex(),
};
let (w, h) = match self.direction {
DividerDirection::Horizontal => (px(segment_min_w), px(1.)),
DividerDirection::Vertical => (px(1.), px(segment_min_w)),
};
let color = self.color.hsla(cx);
let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap
base.min_w(px(total_min_w))
.map(|this| {
if self.direction == DividerDirection::Horizontal {
this.w_full().h_px()
} else {
this.w_px().h_full()
}
})
.gap(px(segment_min_w))
.overflow_hidden()
.children(
(0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)),
)
}
}

View File

@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
\n\nFacepiles are used to display a group of people or things,\
such as a list of participants in a collaboration session."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let few_faces: [&'static str; 3] = [
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",

View File

@@ -169,6 +169,7 @@ pub enum IconName {
Ellipsis,
EllipsisVertical,
Envelope,
Eraser,
Escape,
ExpandVertical,
Exit,
@@ -177,6 +178,7 @@ pub enum IconName {
File,
FileCode,
FileDoc,
FileDiff,
FileGeneric,
FileGit,
FileLock,
@@ -198,6 +200,8 @@ pub enum IconName {
GenericRestore,
Github,
Globe,
GitBranch,
Github,
Hash,
HistoryRerun,
Indicator,
@@ -222,6 +226,8 @@ pub enum IconName {
Option,
PageDown,
PageUp,
PanelLeft,
PanelRight,
Pencil,
Person,
PhoneIncoming,
@@ -231,6 +237,9 @@ pub enum IconName {
PocketKnife,
Public,
PullRequest,
PhoneIncoming,
PanelLeft,
PanelRight,
Quote,
RefreshTitle,
Regex,
@@ -265,6 +274,9 @@ pub enum IconName {
SparkleFilled,
Spinner,
Split,
SquareDot,
SquareMinus,
SquarePlus,
Star,
StarFilled,
Stop,
@@ -277,6 +289,8 @@ pub enum IconName {
Tab,
Terminal,
TextSnippet,
ThumbsUp,
ThumbsDown,
Trash,
TrashAlt,
Triangle,
@@ -493,7 +507,7 @@ impl RenderOnce for IconDecoration {
}
impl ComponentPreview for IconDecoration {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
let examples = all_kinds
@@ -535,7 +549,7 @@ impl RenderOnce for DecoratedIcon {
}
impl ComponentPreview for DecoratedIcon {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let icon_1 = Icon::new(IconName::FileDoc);
let icon_2 = Icon::new(IconName::FileDoc);
let icon_3 = Icon::new(IconName::FileDoc);
@@ -654,7 +668,7 @@ impl RenderOnce for IconWithIndicator {
}
impl ComponentPreview for Icon {
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,

View File

@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
"An indicator visually represents a status or state."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Types",

View File

@@ -39,6 +39,7 @@ pub struct ListItem {
children: SmallVec<[AnyElement; 2]>,
selectable: bool,
overflow_x: bool,
focused: Option<bool>,
}
impl ListItem {
@@ -62,6 +63,7 @@ impl ListItem {
children: SmallVec::new(),
selectable: true,
overflow_x: false,
focused: None,
}
}
@@ -140,6 +142,11 @@ impl ListItem {
self.overflow_x = true;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = Some(focused);
self
}
}
impl Disableable for ListItem {
@@ -177,9 +184,14 @@ impl RenderOnce for ListItem {
this
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border_1()
// .border_color(cx.theme().colors().border_focused)
// })
.when_some(self.focused, |this, focused| {
if focused {
this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| {
this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
@@ -204,10 +216,15 @@ impl RenderOnce for ListItem {
.when(self.inset && !self.disabled, |this| {
this
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border_1()
// .border_color(cx.theme().colors().border_focused)
// })
//.when(self.state == InteractionState::Focused, |this| {
.when_some(self.focused, |this, focused| {
if focused {
this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| {
this.hover(|style| {
style.bg(cx.theme().colors().ghost_element_hover)

View File

@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
ExampleLabelSide::Top
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(vec![
single_example(

View File

@@ -30,20 +30,20 @@ pub trait ComponentPreview: IntoElement {
ExampleLabelSide::default()
}
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
fn component_previews(cx: &mut WindowContext) -> Vec<AnyElement> {
Self::examples(cx)
.into_iter()
.map(|example| Self::render_example_group(example))
.collect()
}
fn render_component_previews(cx: &WindowContext) -> AnyElement {
fn render_component_previews(cx: &mut WindowContext) -> AnyElement {
let title = Self::title();
let (source, title) = title
.rsplit_once("::")

View File

@@ -24,6 +24,7 @@ futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true

View File

@@ -8,6 +8,7 @@ pub mod test;
use futures::Future;
use itertools::Either;
use regex::Regex;
use std::sync::OnceLock;
use std::{
@@ -199,6 +200,35 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
}
}
pub fn iterate_expanded_and_wrapped_usize_range(
range: Range<usize>,
additional_before: usize,
additional_after: usize,
wrap_length: usize,
) -> impl Iterator<Item = usize> {
let start_wraps = range.start < additional_before;
let end_wraps = wrap_length < range.end + additional_after;
if start_wraps && end_wraps {
Either::Left(0..wrap_length)
} else if start_wraps {
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
if wrapped_start <= range.end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
}
} else if end_wraps {
let wrapped_end = range.end + additional_after - wrap_length;
if range.start <= wrapped_end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
}
} else {
Either::Left((range.start - additional_before)..(range.end + additional_after))
}
}
pub trait ResultExt<E> {
type Ok;
@@ -733,4 +763,48 @@ Line 2
Line 3"#
);
}
#[test]
fn test_iterate_expanded_and_wrapped_usize_range() {
// Neither wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
(1..5).collect::<Vec<usize>>()
);
// Start wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
((0..5).chain(7..8)).collect::<Vec<usize>>()
);
// Start wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Start wraps all the way around and past 0
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
(0..1).chain(2..8).collect::<Vec<usize>>()
);
// End wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps all the way around and past the end
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Both start and end wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
}
}

View File

@@ -71,11 +71,12 @@ pub enum ShowDiagnostics {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[serde(rename_all = "snake_case")]
pub enum ActivateOnClose {
#[default]
History,
Neighbour,
LeftNeighbour,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -1506,6 +1506,7 @@ impl Pane {
self.pinned_tab_count -= 1;
}
if item_index == self.active_item_index {
let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
let index_to_activate = match activate_on_close {
ActivateOnClose::History => self
.activation_history
@@ -1517,7 +1518,7 @@ impl Pane {
})
// We didn't have a valid activation history entry, so fallback
// to activating the item to the left
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)),
.unwrap_or_else(left_neighbour_index),
ActivateOnClose::Neighbour => {
self.activation_history.pop();
if item_index + 1 < self.items.len() {
@@ -1526,6 +1527,10 @@ impl Pane {
item_index.saturating_sub(1)
}
}
ActivateOnClose::LeftNeighbour => {
self.activation_history.pop();
left_neighbour_index()
}
};
let should_activate = activate_pane || self.has_focus(cx);
@@ -3666,6 +3671,69 @@ mod tests {
assert_item_labels(&pane, ["A*"], cx);
}
#[gpui::test]
async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
init_test(cx);
cx.update_global::<SettingsStore, ()>(|s, cx| {
s.update_user_settings::<ItemSettings>(cx, |s| {
s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
});
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", false, cx);
add_labeled_item(&pane, "B", false, cx);
add_labeled_item(&pane, "C", false, cx);
add_labeled_item(&pane, "D", false, cx);
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
add_labeled_item(&pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update(cx, |pane, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update(cx, |pane, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B", "C*"], cx);
pane.update(cx, |pane, cx| pane.activate_item(0, false, false, cx));
assert_item_labels(&pane, ["A*", "B", "C"], cx);
pane.update(cx, |pane, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["B*", "C"], cx);
pane.update(cx, |pane, cx| {
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["C*"], cx);
}
#[gpui::test]
async fn test_close_inactive_items(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -502,7 +502,7 @@ impl ThemePreview {
)
}
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
fn render_components_page(&self, cx: &mut WindowContext) -> impl IntoElement {
let layer = ElevationIndex::Surface;
v_flex()
@@ -520,8 +520,8 @@ impl ThemePreview {
.child(Indicator::render_component_previews(cx))
.child(Icon::render_component_previews(cx))
.child(Table::render_component_previews(cx))
.child(self.render_avatars(cx))
.child(self.render_buttons(layer, cx))
// .child(self.render_avatars(cx))
// .child(self.render_buttons(layer, cx))
}
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {

View File

@@ -52,6 +52,7 @@ file_icons.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
git_ui.workspace = true
git_hosting_providers.workspace = true
go_to_line.workspace = true
gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }

View File

@@ -442,6 +442,7 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
git_ui::git_panel::init(cx);
outline_panel::init(Assets, cx);
tasks_ui::init(cx);
snippets_ui::init(cx);
@@ -463,6 +464,7 @@ fn main() {
welcome::init(cx);
settings_ui::init(cx);
extensions_ui::init(cx);
zeta::init(cx);
cx.observe_global::<SettingsStore>({
let languages = app_state.languages.clone();

View File

@@ -21,6 +21,9 @@ use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, select_biased, StreamExt};
use git_ui::git_panel::GitPanel;
use git_ui::GitStatusIndicator;
use git_ui::{git_panel, GitStatusIndicator};
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem,
PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext,
@@ -202,6 +205,8 @@ pub fn initialize_workspace(
let diagnostic_summary =
cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let git_indicator =
cx.new_view(|cx| GitStatusIndicator::new(workspace, cx));
let activity_indicator =
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
let active_buffer_language =
@@ -212,6 +217,7 @@ pub fn initialize_workspace(
let cursor_position =
cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(git_indicator, cx);
status_bar.add_left_item(diagnostic_summary, cx);
status_bar.add_left_item(activity_indicator, cx);
status_bar.add_right_item(inline_completion_button, cx);
@@ -239,7 +245,14 @@ pub fn initialize_workspace(
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
let prompt_builder = prompt_builder.clone();
let git_panel = cx.new_view(|cx| GitPanel::new("git-panel", cx));
cx.spawn(|workspace_handle, mut cx| async move {
let assistant_panel =
assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
@@ -269,6 +282,9 @@ pub fn initialize_workspace(
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(git_panel, cx);
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(outline_panel, cx);
workspace.add_panel(terminal_panel, cx);

View File

@@ -4,9 +4,9 @@ use client::Client;
use collections::HashMap;
use copilot::{Copilot, CopilotCompletionProvider};
use editor::{Editor, EditorMode};
use feature_flags::FeatureFlagAppExt;
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use gpui::{AnyWindowHandle, AppContext, Context, ViewContext, WeakView};
use language::language_settings::all_language_settings;
use language::language_settings::{all_language_settings, InlineCompletionProvider};
use settings::SettingsStore;
use supermaven::{Supermaven, SupermavenCompletionProvider};
@@ -49,20 +49,56 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
});
}
cx.observe_global::<SettingsStore>(move |cx| {
let new_provider = all_language_settings(None, cx).inline_completions.provider;
if new_provider != provider {
provider = new_provider;
for (editor, window) in editors.borrow().iter() {
_ = window.update(cx, |_window, cx| {
_ = editor.update(cx, |editor, cx| {
assign_inline_completion_provider(editor, provider, &client, cx);
})
});
if cx.has_flag::<ZetaFeatureFlag>() {
cx.on_action(clear_zeta_edit_history);
}
cx.observe_flag::<ZetaFeatureFlag, _>({
let editors = editors.clone();
let client = client.clone();
move |active, cx| {
let provider = all_language_settings(None, cx).inline_completions.provider;
assign_inline_completion_providers(&editors, provider, &client, cx);
if active && !cx.is_action_available(&zeta::ClearHistory) {
cx.on_action(clear_zeta_edit_history);
}
}
})
.detach();
cx.observe_global::<SettingsStore>({
let editors = editors.clone();
let client = client.clone();
move |cx| {
let new_provider = all_language_settings(None, cx).inline_completions.provider;
if new_provider != provider {
provider = new_provider;
assign_inline_completion_providers(&editors, provider, &client, cx)
}
}
})
.detach();
}
fn clear_zeta_edit_history(_: &zeta::ClearHistory, cx: &mut AppContext) {
if let Some(zeta) = zeta::Zeta::global(cx) {
zeta.update(cx, |zeta, _| zeta.clear_history());
}
}
fn assign_inline_completion_providers(
editors: &Rc<RefCell<HashMap<WeakView<Editor>, AnyWindowHandle>>>,
provider: InlineCompletionProvider,
client: &Arc<Client>,
cx: &mut AppContext,
) {
for (editor, window) in editors.borrow().iter() {
_ = window.update(cx, |_window, cx| {
_ = editor.update(cx, |editor, cx| {
assign_inline_completion_provider(editor, provider, &client, cx);
})
});
}
}
fn register_backward_compatible_actions(editor: &mut Editor, cx: &ViewContext<Editor>) {
@@ -129,7 +165,7 @@ fn assign_inline_completion_provider(
}
}
language::language_settings::InlineCompletionProvider::Zeta => {
if cx.is_staff() {
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
let zeta = zeta::Zeta::register(client.clone(), cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).file().is_some() {

View File

@@ -13,6 +13,9 @@ workspace = true
path = "src/zeta.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
client.workspace = true
@@ -21,6 +24,7 @@ editor.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true
language.workspace = true
language_models.workspace = true
@@ -32,8 +36,8 @@ settings.workspace = true
similar.workspace = true
telemetry_events.workspace = true
theme.workspace = true
util.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true

View File

@@ -1,18 +1,44 @@
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor;
use gpui::{
prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle,
Model, StyledText, TextStyle, View, ViewContext,
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
};
use language::{language_settings, OffsetRangeExt};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ListItem, ListItemSpacing};
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, TintColor, Tooltip};
use workspace::{ModalView, Workspace};
actions!(
zeta,
[
RateCompletions,
ThumbsUp,
ThumbsDown,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
PreviousEdit,
FocusCompletions,
PreviewCompletion,
]
);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &RateCompletions, cx| {
RateCompletionModal::toggle(workspace, cx);
});
})
.detach();
}
pub struct RateCompletionModal {
zeta: Model<Zeta>,
active_completion: Option<ActiveCompletion>,
selected_index: usize,
focus_handle: FocusHandle,
_subscription: gpui::Subscription,
}
@@ -33,6 +59,7 @@ impl RateCompletionModal {
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
Self {
zeta,
selected_index: 0,
focus_handle: cx.focus_handle(),
active_completion: None,
_subscription: subscription,
@@ -43,15 +70,194 @@ impl RateCompletionModal {
cx.emit(DismissEvent);
}
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).recent_completions().count(),
);
cx.notify();
}
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
self.selected_index = self.selected_index.saturating_sub(1);
cx.notify();
}
fn select_next_edit(&mut self, _: &NextEdit, cx: &mut ViewContext<Self>) {
let next_index = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| ix + self.selected_index);
if let Some(next_index) = next_index {
self.selected_index = next_index;
cx.notify();
}
}
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.recent_completions_len();
let prev_index = self
.zeta
.read(cx)
.recent_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
.skip(1) // Skip straight to the previous item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| self.selected_index - ix);
if let Some(prev_index) = prev_index {
self.selected_index = prev_index;
cx.notify();
}
cx.notify();
}
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = 0;
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
cx.notify();
}
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
let completion = zeta
.recent_completions()
.skip(self.selected_index)
.next()
.cloned();
if let Some(completion) = completion {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
"".to_string(),
cx,
);
}
});
self.select_next_edit(&Default::default(), cx);
cx.notify();
}
fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
if let Some(active) = &self.active_completion {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Positive,
active.feedback_editor.read(cx).text(cx),
cx,
);
}
});
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
if let Some(active) = &self.active_completion {
if active.feedback_editor.read(cx).text(cx).is_empty() {
return;
}
self.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Negative,
active.feedback_editor.read(cx).text(cx),
cx,
);
});
}
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn focus_completions(&mut self, _: &FocusCompletions, cx: &mut ViewContext<Self>) {
cx.focus_self();
cx.notify();
}
fn preview_completion(&mut self, _: &PreviewCompletion, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, false, cx);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, true, cx);
}
pub fn select_completion(
&mut self,
completion: Option<InlineCompletion>,
focus: bool,
cx: &mut ViewContext<Self>,
) {
// Avoid resetting completion rating if it's already selected.
if let Some(completion) = completion.as_ref() {
self.selected_index = self
.zeta
.read(cx)
.recent_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
.unwrap_or(self.selected_index);
cx.notify();
if let Some(prev_completion) = self.active_completion.as_ref() {
if completion.id == prev_completion.completion.id {
if focus {
cx.focus_view(&prev_completion.feedback_editor);
}
return;
}
}
@@ -69,10 +275,14 @@ impl RateCompletionModal {
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_inline_completions(Some(false), cx);
editor.set_placeholder_text("Your feedback about this completion...", cx);
editor.set_placeholder_text("Add your feedback", cx);
if focus {
cx.focus_self();
}
editor
}),
});
cx.notify();
}
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
@@ -134,94 +344,131 @@ impl RateCompletionModal {
};
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
let feedback_empty = active_completion
.feedback_editor
.read(cx)
.text(cx)
.is_empty();
let border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let label_container = || h_flex().pl_1().gap_1p5();
Some(
v_flex()
.flex_1()
.size_full()
.gap_2()
.child(h_flex().justify_center().children(if rated {
Some(
Label::new("This completion was already rated")
.color(Color::Muted)
.size(LabelSize::Large),
)
} else if active_completion.completion.edits.is_empty() {
Some(
Label::new("This completion didn't produce any edits")
.color(Color::Warning)
.size(LabelSize::Large),
)
} else {
None
}))
.overflow_hidden()
.child(
v_flex()
div()
.id("diff")
.flex_1()
.flex_basis(relative(0.75))
.bg(cx.theme().colors().editor_background)
.overflow_y_scroll()
.p_2()
.border_color(cx.theme().colors().border)
.border_1()
.rounded_lg()
.py_4()
.px_6()
.size_full()
.bg(bg_color)
.overflow_scroll()
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
)
.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
.child(
div()
.flex_1()
.flex_basis(relative(0.25))
.bg(cx.theme().colors().editor_background)
.border_color(cx.theme().colors().border)
.border_1()
.rounded_lg()
.h_40()
.pt_1()
.bg(bg_color)
.child(active_completion.feedback_editor.clone()),
)
.child(
h_flex()
.gap_2()
.justify_end()
.p_1()
.h_8()
.border_t_1()
.border_color(border_color)
.max_w_full()
.justify_between()
.children(if rated {
Some(
label_container()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Rated completion").color(Color::Muted)),
)
} else if active_completion.completion.edits.is_empty() {
Some(
label_container()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("No edits produced").color(Color::Muted)),
)
} else {
Some(label_container())
})
.child(
Button::new("bad", "👎 Bad Completion")
.size(ButtonSize::Large)
.disabled(rated)
.label_size(LabelSize::Large)
.color(Color::Error)
.on_click({
let completion = active_completion.completion.clone();
let feedback_editor = active_completion.feedback_editor.clone();
cx.listener(move |this, _, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&completion,
InlineCompletionRating::Negative,
feedback_editor.read(cx).text(cx),
cx,
)
h_flex()
.gap_1()
.child(
Button::new("bad", "Bad Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsDown,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Negative))
.icon(IconName::ThumbsDown)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Error)
.disabled(rated || feedback_empty)
.when(feedback_empty, |this| {
this.tooltip(|cx| {
Tooltip::text("Explain why this completion is bad before reporting it", cx)
})
})
})
}),
)
.child(
Button::new("good", "👍 Good Completion")
.size(ButtonSize::Large)
.disabled(rated)
.label_size(LabelSize::Large)
.color(Color::Success)
.on_click({
let completion = active_completion.completion.clone();
let feedback_editor = active_completion.feedback_editor.clone();
cx.listener(move |this, _, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
feedback_editor.read(cx).text(cx),
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_down_active(
&ThumbsDownActiveCompletion,
cx,
)
})
})
}),
);
})),
)
.child(
Button::new("good", "Good Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsUp,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Positive))
.icon(IconName::ThumbsUp)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Success)
.disabled(rated)
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
})),
),
),
),
)
@@ -230,30 +477,53 @@ impl RateCompletionModal {
impl Render for RateCompletionModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let border_color = cx.theme().colors().border;
h_flex()
.gap_2()
.bg(cx.theme().colors().elevated_surface_background)
.w(cx.viewport_size().width - px(256.))
.h(cx.viewport_size().height - px(256.))
.rounded_lg()
.shadow_lg()
.p_2()
.key_context("RateCompletionModal")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_prev_edit))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_next_edit))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::thumbs_up))
.on_action(cx.listener(Self::thumbs_up_active))
.on_action(cx.listener(Self::thumbs_down_active))
.on_action(cx.listener(Self::focus_completions))
.on_action(cx.listener(Self::preview_completion))
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(border_color)
.w(cx.viewport_size().width - px(320.))
.h(cx.viewport_size().height - px(300.))
.rounded_lg()
.shadow_lg()
.child(
div()
.id("completion_list")
.border_r_1()
.border_color(border_color)
.w_96()
.h_full()
.p_0p5()
.overflow_y_scroll()
.child(
ui::List::new()
List::new()
.empty_message(
"No completions, use the editor to generate some and rate them!",
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some and rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).recent_completions().cloned().map(
|completion| {
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id
@@ -261,25 +531,30 @@ impl Render for RateCompletionModal {
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected)
.end_slot(if rated {
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success)
} else if completion.edits.is_empty() {
Icon::new(IconName::Ellipsis).color(Color::Muted)
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
} else {
Icon::new(IconName::Diff).color(Color::Muted)
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
})
.child(Label::new(
completion.path.to_string_lossy().to_string(),
))
).size(LabelSize::Small))
.child(
Label::new(format!("({})", completion.id))
.color(Color::Muted)
.size(LabelSize::XSmall),
div()
.overflow_hidden()
.text_ellipsis()
.child(Label::new(format!("({})", completion.id))
.color(Color::Muted)
.size(LabelSize::XSmall)),
)
.on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), cx);
this.select_completion(Some(completion.clone()), true, cx);
}))
},
)),

View File

@@ -6,18 +6,21 @@ use anyhow::{anyhow, Context as _, Result};
use client::Client;
use collections::{HashMap, HashSet, VecDeque};
use futures::AsyncReadExt;
use gpui::{AppContext, Context, Global, Model, ModelContext, Subscription, Task};
use gpui::{
actions, AppContext, Context, EntityId, Global, Model, ModelContext, Subscription, Task,
};
use http_client::{HttpClient, Method};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
Point, ToOffset, ToPoint,
};
use language_models::LlmApiToken;
use rpc::{PredictEditsParams, PredictEditsResponse};
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
use std::{
borrow::Cow,
cmp,
fmt::Write,
future::Future,
mem,
ops::Range,
path::Path,
@@ -34,6 +37,8 @@ const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>";
const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
actions!(zeta, [ClearHistory]);
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
pub struct InlineCompletionId(Uuid);
@@ -155,6 +160,10 @@ impl Zeta {
})
}
pub fn clear_history(&mut self) {
self.events.clear();
}
fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx);
@@ -247,12 +256,17 @@ impl Zeta {
}
}
pub fn request_completion(
pub fn request_completion_impl<F, R>(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
perform_predict_edits: F,
) -> Task<Result<InlineCompletion>>
where
F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static,
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
{
let snapshot = self.report_changes_for_buffer(buffer, cx);
let point = position.to_point(&snapshot);
let offset = point.to_offset(&snapshot);
@@ -269,8 +283,6 @@ impl Zeta {
cx.spawn(|this, mut cx| async move {
let start = std::time::Instant::now();
let token = llm_token.acquire(&client).await?;
let mut input_events = String::new();
for event in events {
if !input_events.is_empty() {
@@ -283,130 +295,26 @@ impl Zeta {
log::debug!("Events:\n{}\nExcerpt:\n{}", input_events, input_excerpt);
let http_client = client.http_client();
let body = PredictEditsParams {
input_events: input_events.clone(),
input_excerpt: input_excerpt.clone(),
};
let request_builder = http_client::Request::builder();
let request = request_builder
.method(Method::POST)
.uri(
client
.http_client()
.build_zed_llm_url("/predict_edits", &[])?
.as_ref(),
)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(serde_json::to_string(&body)?.into())?;
let mut response = http_client.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if !response.status().is_success() {
return Err(anyhow!(
"error predicting edits.\nStatus: {:?}\nBody: {}",
response.status(),
body
));
}
let response = serde_json::from_str::<PredictEditsResponse>(&body)?;
let response = perform_predict_edits(client, llm_token, body).await?;
let output_excerpt = response.output_excerpt;
log::debug!("prediction took: {:?}", start.elapsed());
log::debug!("completion response: {}", output_excerpt);
let content = output_excerpt.replace(CURSOR_MARKER, "");
let mut new_text = content.as_str();
let codefence_start = new_text
.find(EDITABLE_REGION_START_MARKER)
.context("could not find start marker")?;
new_text = &new_text[codefence_start..];
let newline_ix = new_text.find('\n').context("could not find newline")?;
new_text = &new_text[newline_ix + 1..];
let codefence_end = new_text
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
new_text = &new_text[..codefence_end];
log::debug!("sanitized completion response: {}", new_text);
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
let diff = similar::TextDiff::from_chars(old_text.as_str(), new_text);
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
let mut old_start = excerpt_range.start;
for change in diff.iter_all_changes() {
let value = change.value();
match change.tag() {
similar::ChangeTag::Equal => {
old_start += value.len();
}
similar::ChangeTag::Delete => {
let old_end = old_start + value.len();
if let Some((last_old_range, _)) = edits.last_mut() {
if last_old_range.end == old_start {
last_old_range.end = old_end;
} else {
edits.push((old_start..old_end, String::new()));
}
} else {
edits.push((old_start..old_end, String::new()));
}
old_start = old_end;
}
similar::ChangeTag::Insert => {
if let Some((last_old_range, last_new_text)) = edits.last_mut() {
if last_old_range.end == old_start {
last_new_text.push_str(value);
} else {
edits.push((old_start..old_start, value.into()));
}
} else {
edits.push((old_start..old_start, value.into()));
}
}
}
}
let edits = edits
.into_iter()
.map(|(mut old_range, new_text)| {
let prefix_len = common_prefix(
snapshot.chars_for_range(old_range.clone()),
new_text.chars(),
);
old_range.start += prefix_len;
let suffix_len = common_prefix(
snapshot.reversed_chars_for_range(old_range.clone()),
new_text[prefix_len..].chars().rev(),
);
old_range.end = old_range.end.saturating_sub(suffix_len);
let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string();
(
snapshot.anchor_after(old_range.start)
..snapshot.anchor_before(old_range.end),
new_text,
)
})
.collect();
let inline_completion = InlineCompletion {
id: InlineCompletionId::new(),
path,
let inline_completion = Self::process_completion_response(
output_excerpt,
&snapshot,
excerpt_range,
edits,
snapshot,
input_events: input_events.into(),
input_excerpt: input_excerpt.into(),
output_excerpt: output_excerpt.into(),
};
path,
input_events,
input_excerpt,
)?;
this.update(&mut cx, |this, cx| {
this.recent_completions
.push_front(inline_completion.clone());
@@ -420,6 +328,321 @@ impl Zeta {
})
}
// Generates several example completions of various states to fill the Zeta completion modal
#[cfg(any(test, feature = "test-support"))]
pub fn fill_with_fake_completions(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
"#};
let buffer = cx.new_model(|cx| Buffer::local(test_buffer_text, cx));
let position = buffer.read(cx).anchor_before(Point::new(1, 0));
let completion_tasks = vec![
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
[here's an edit]
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
", ),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
[and another edit]
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
[a third completion]
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
and then another
[fourth completion example]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
[fifth and final completion]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
];
cx.spawn(|zeta, mut cx| async move {
for task in completion_tasks {
task.await.unwrap();
}
zeta.update(&mut cx, |zeta, _cx| {
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
})
.ok();
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
response: PredictEditsResponse,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
use std::future::ready;
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
}
pub fn request_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
}
fn perform_predict_edits(
client: Arc<Client>,
llm_token: LlmApiToken,
body: PredictEditsParams,
) -> impl Future<Output = Result<PredictEditsResponse>> {
async move {
let http_client = client.http_client();
let mut token = llm_token.acquire(&client).await?;
let mut did_retry = false;
loop {
let request_builder = http_client::Request::builder();
let request = request_builder
.method(Method::POST)
.uri(
http_client
.build_zed_llm_url("/predict_edits", &[])?
.as_ref(),
)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(serde_json::to_string(&body)?.into())?;
let mut response = http_client.send(request).await?;
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Ok(serde_json::from_str(&body)?);
} else if !did_retry
&& response
.headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some()
{
did_retry = true;
token = llm_token.refresh(&client).await?;
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow!(
"error predicting edits.\nStatus: {:?}\nBody: {}",
response.status(),
body
));
}
}
}
}
fn process_completion_response(
output_excerpt: String,
snapshot: &BufferSnapshot,
excerpt_range: Range<usize>,
path: Arc<Path>,
input_events: String,
input_excerpt: String,
) -> Result<InlineCompletion> {
let content = output_excerpt.replace(CURSOR_MARKER, "");
let codefence_start = content
.find(EDITABLE_REGION_START_MARKER)
.context("could not find start marker")?;
let content = &content[codefence_start..];
let newline_ix = content.find('\n').context("could not find newline")?;
let content = &content[newline_ix + 1..];
let codefence_end = content
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
let new_text = &content[..codefence_end];
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, snapshot);
Ok(InlineCompletion {
id: InlineCompletionId::new(),
path,
excerpt_range,
edits: edits.into(),
snapshot: snapshot.clone(),
input_events: input_events.into(),
input_excerpt: input_excerpt.into(),
output_excerpt: output_excerpt.into(),
})
}
pub fn compute_edits(
old_text: String,
new_text: &str,
offset: usize,
snapshot: &BufferSnapshot,
) -> Vec<(Range<Anchor>, String)> {
let diff = similar::TextDiff::from_words(old_text.as_str(), new_text);
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
let mut old_start = offset;
for change in diff.iter_all_changes() {
let value = change.value();
match change.tag() {
similar::ChangeTag::Equal => {
old_start += value.len();
}
similar::ChangeTag::Delete => {
let old_end = old_start + value.len();
if let Some((last_old_range, _)) = edits.last_mut() {
if last_old_range.end == old_start {
last_old_range.end = old_end;
} else {
edits.push((old_start..old_end, String::new()));
}
} else {
edits.push((old_start..old_end, String::new()));
}
old_start = old_end;
}
similar::ChangeTag::Insert => {
if let Some((last_old_range, last_new_text)) = edits.last_mut() {
if last_old_range.end == old_start {
last_new_text.push_str(value);
} else {
edits.push((old_start..old_start, value.into()));
}
} else {
edits.push((old_start..old_start, value.into()));
}
}
}
}
edits
.into_iter()
.map(|(mut old_range, new_text)| {
let prefix_len = common_prefix(
snapshot.chars_for_range(old_range.clone()),
new_text.chars(),
);
old_range.start += prefix_len;
let suffix_len = common_prefix(
snapshot.reversed_chars_for_range(old_range.clone()),
new_text[prefix_len..].chars().rev(),
);
old_range.end = old_range.end.saturating_sub(suffix_len);
let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string();
(
snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end),
new_text,
)
})
.collect()
}
pub fn is_completion_rated(&self, completion_id: InlineCompletionId) -> bool {
self.rated_completions.contains(&completion_id)
}
@@ -445,10 +668,14 @@ impl Zeta {
cx.notify();
}
pub fn recent_completions(&self) -> impl Iterator<Item = &InlineCompletion> {
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.recent_completions.iter()
}
pub fn recent_completions_len(&self) -> usize {
self.recent_completions.len()
}
fn report_changes_for_buffer(
&mut self,
buffer: &Model<Buffer>,
@@ -610,9 +837,14 @@ impl Event {
}
}
struct CurrentInlineCompletion {
buffer_id: EntityId,
completion: InlineCompletion,
}
pub struct ZetaInlineCompletionProvider {
zeta: Model<Zeta>,
current_completion: Option<InlineCompletion>,
current_completion: Option<CurrentInlineCompletion>,
pending_refresh: Task<()>,
}
@@ -653,28 +885,34 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
debounce: bool,
cx: &mut ModelContext<Self>,
) {
self.pending_refresh = cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
self.pending_refresh =
cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
})
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = completion_request.await.log_err().map(|completion| {
CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion,
}
});
}
this.update(&mut cx, |this, cx| {
this.current_completion = completion;
cx.notify();
})
.ok();
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = completion_request.await.log_err();
}
this.update(&mut cx, |this, cx| {
this.current_completion = completion;
cx.notify();
})
.ok();
});
}
fn cycle(
@@ -699,7 +937,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
cursor_position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Option<inline_completion::InlineCompletion> {
let completion = self.current_completion.as_mut()?;
let CurrentInlineCompletion {
buffer_id,
completion,
} = self.current_completion.as_mut()?;
// Invalidate previous completion if it was generated for a different buffer.
if *buffer_id != buffer.entity_id() {
self.current_completion.take();
return None;
}
let buffer = buffer.read(cx);
let Some(edits) = completion.interpolate(buffer.snapshot()) else {

View File

@@ -56,6 +56,8 @@ You can customize a wide range of settings for each language, including:
- [`hard_tabs`](./configuring-zed.md#hard-tabs): Use tabs instead of spaces for indentation
- [`preferred_line_length`](./configuring-zed.md#preferred-line-length): The recommended maximum line length
- [`soft_wrap`](./configuring-zed.md#soft-wrap): How to wrap long lines of code
- [`show_completions_on_input`](./configuring-zed.md#show-completions-on-input): Whether or not to show completions as you type
- [`show_completion_documentation`](./configuring-zed.md#show-completion-documentation): Whether to display inline and alongside documentation for items in the completions menu
These settings allow you to maintain specific coding styles across different languages and projects.

View File

@@ -691,7 +691,7 @@ List of `string` values
}
```
2. Activate the neighbour tab (prefers the right one, if present):
2. Activate the right neighbour tab if present:
```json
{
@@ -699,6 +699,14 @@ List of `string` values
}
```
3. Activate the left neighbour tab if present:
```json
{
"activate_on_close": "left_neighbour"
}
```
### Always show the close button
- Description: Whether to always show the close button on tabs.
@@ -994,7 +1002,7 @@ The result is still `)))` and not `))))))`, which is what it would be by default
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj"
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
@@ -1517,16 +1525,6 @@ Or to set a `socks5` proxy:
`boolean` values
## Completion Documentation Debounce Delay
- Description: The debounce delay before re-querying the language server for completion documentation when not included in original completion list.
- Setting: `completion_documentation_secondary_query_debounce`
- Default: `300` ms
**Options**
`integer` values
## Show Inline Completions
- Description: Whether to show inline completions as you type or manually by triggering `editor::ShowInlineCompletion`.

View File

@@ -14,6 +14,16 @@ CompileFlags:
Add: [-xc]
```
By default clang and gcc by will recognize `*.C` and `*.H` (uppercase extensions) as C++ and not C and so Zed too follows this convention. If you are working with a C-only project (perhaps one with legacy uppercase pathing like `FILENAME.C`) you can override this behavior by adding this to your settings:
```json
{
"file_types": {
"C": ["C", "H"]
}
}
```
## Formatting
By default Zed will use the `clangd` language server for formatting C code. The Clangd is the same as the `clang-format` CLI tool. To configure this you can add a `.clang-format` file. For example:

View File

@@ -77,7 +77,7 @@ You can trigger formatting via {#kb editor::Format} or the `editor: format` acti
```json
"languages": {
"C++" {
"C++": {
"format_on_save": "on",
"tab_size": 2
}

View File

@@ -35,4 +35,20 @@ If you would like to use a specific dart binary or use dart via FVM you can spec
}
```
### Formatting
Dart by-default uses a very conservative maximum line length (80). If you would like the dart LSP to permit a longer line length when auto-formatting, add the following to your Zed settings.json:
```json
{
"lsp": {
"dart": {
"settings": {
"lineLength": 140
}
}
}
}
```
Please see the Dart documentation for more information on [dart language-server capabilities](https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md).

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