Compare commits

..

149 Commits

Author SHA1 Message Date
Mikayla
fdc5999d79 test 2024-07-29 13:00:59 -07:00
Anthony Eid
74931bd472 Make active debug line stand out more & set unique DAP client ids (#8) 2024-07-29 19:27:07 +02:00
Remco Smits
7f8c28877f Wip start debugging request 2024-07-27 10:46:55 +02:00
Remco Smits
1ff23477de Clean up run in terminal code 2024-07-27 10:45:31 +02:00
Remco Smits
d28950c633 Line up toggle debug panel focus with other panels action names 2024-07-26 23:29:08 +02:00
Remco Smits
6ff5e00740 Fix don't go to stack frame if only the thread id matches of the stopped event
This makes sure if you have multiple debug adapters running we don't match a thread id that belongs to a different client
2024-07-25 22:21:27 +02:00
Remco Smits
b70acdfa4a Wip run in terminal request 2024-07-25 21:30:49 +02:00
Remco Smits
403ae10087 Fix infinite loop of threads, because sequence id was wrong
This was added to make sure we had the same sequence id as python debugger. But resulting in breaking xdebug(php) debug adapter
2024-07-25 21:22:51 +02:00
Anthony Eid
9a8a54109e Fix race condition in debugger (#6)
* Add support for DAP to use std for communication

* Add more descriptive error logs for DAP

* Implement handler for continued event

* Add PR feedback to handle_continued_event function

* Updated debugger

* Fix race condition when using late case debug adapters

The main thing this commit does is fix race conditions between a
dap client's event handler and sending a launch/attach request.
Some adapters would only respond to a starting request after the
client handled an init event, which could never happen because
the client used to start it's handler after it sent a launch request.

This commit also ignores undefined errors instead of crashing. This
allows the client to work with adapters that send custom events.

Finially, I added some more descriptive error messages and change
client's starting request seq from 0 to 1 to be more in line with
the specs.

* Get clippy to run successfully

* Add some function docs to dap client

* Fix debugger's highlight when stepping through code

Merging with the main zed branch removed a function that
the debugger panel relied on. I added the function back
and changed it to work with the updated internals of zed.

* Get clippy to pass & add an error log instead of using dbg!()

* Get sequence id to be incremented after getting respond and event

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2024-07-25 20:24:19 +02:00
Remco Smits
11c740b47a Merge branch 'main' into debugger 2024-07-25 20:03:06 +02:00
Remco Smits
1a421db92d Make clippy happier than ever 2024-07-24 13:05:14 +02:00
Remco Smits
9c60884771 Merge branch 'main' into debugger 2024-07-24 12:53:44 +02:00
Remco Smits
ee3323d12a Make clippy happy again 2024-07-24 12:14:18 +02:00
Remco Smits
b88fd3e0c5 Make sure we read events as early as possible
We have to read them as early as possible to make sure we support more debug adapters. Some of them wait before you handled the initialized event. That is also why I moved the launch/attach request to be directly after the initialize request. For example the xdebug adapter sends the initialized event only when you send the launch request, so before if we send the breakpoints and configuration is done requests the adapter would stop and all channels were closed.
2024-07-24 11:51:03 +02:00
Remco Smits
2e10853b34 Fix sending incorrect start request id 2024-07-23 14:45:04 +02:00
Remco Smits
d08e28f4e0 Clean up show continue/pause buttons 2024-07-17 15:43:16 +02:00
Remco Smits
ef67321ff2 Implement restart program 2024-07-17 14:41:22 +02:00
Remco Smits
4c777ad140 Fix deserialize error when debug adapter send empty {} body but expected type () 2024-07-17 14:09:56 +02:00
Remco Smits
923ae5473a Fix that we change the thread state status to running
Not all the debug adapters send the continue event so we couldn't determine that the thread state status was `running`. So we have to determine it our selfs if the thread state status did not change, if so we change the status to `running`.

This allows us to show the pause button when the thread state status is running, so you can pause the programs execution.
2024-07-17 10:21:39 +02:00
Remco Smits
a48166e5a0 Add method to get the capabilities from the client 2024-07-16 23:07:43 +02:00
Remco Smits
ef098c028b Show/disable actions buttons based on thread state status 2024-07-16 23:07:29 +02:00
Remco Smits
7ce1b8dc76 Update the thread state status after continue request if continue event was not send 2024-07-16 23:06:39 +02:00
Remco Smits
e350417a33 Add debug pause icon 2024-07-16 22:55:37 +02:00
Remco Smits
ea9e0755df Implement stop action 2024-07-16 21:51:46 +02:00
Remco Smits
6aced1b3aa Add tooltip for tab content to indicate the thread status 2024-07-16 21:29:47 +02:00
Remco Smits
99e01fc608 Clippyyyyy 2024-07-16 21:28:52 +02:00
Remco Smits
fe899c9164 Change that current thread state does not return option 2024-07-16 21:28:36 +02:00
Remco Smits
f8b9937e51 Handle exited event 2024-07-16 21:19:20 +02:00
Remco Smits
dc5928374e Fix we did not notify after stack frame list reset was doen
This fixes an issue if you step debug the same thread, the stack frame list was not up to date.
2024-07-16 21:18:41 +02:00
Remco Smits
0deb3cc606 Move handle initialized event to own method 2024-07-16 21:17:05 +02:00
Remco Smits
ba4a70d7ae Make TCP connect delay configurable 2024-07-16 20:56:24 +02:00
Remco Smits
65a790e4ca Make double click on pane action optional 2024-07-16 20:55:55 +02:00
Remco Smits
b6e677eb06 Wip configure host for tcp debug adapter
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-15 18:44:21 +02:00
Anthony Eid
ffa0609f8d Implement stdio debugger connection type (#5)
* Add support for DAP to use std for communication

* Add more descriptive error logs for DAP

* Implement handler for continued event

* Add PR feedback to handle_continued_event function
2024-07-15 18:12:05 +02:00
Remco Smits
77a314350f Fix autocompletion for connection type
before `s_t_d_i_o` after `stdio`

Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-15 18:03:44 +02:00
Remco Smits
737b03c928 Refactor go_to_stack_frame & clear_hightlights 2024-07-14 16:44:47 +02:00
Remco Smits
1b42dd5865 Add client id to thread tab title 2024-07-14 15:48:43 +02:00
Remco Smits
c9074b1c25 Wire through debugger id for initialize request 2024-07-14 15:19:05 +02:00
Remco Smits
00379280f3 Don't store request type on client(its already in the config) 2024-07-14 15:18:37 +02:00
Remco Smits
4e2d0351cc Rename thread state -> thread states 2024-07-14 15:18:07 +02:00
Remco Smits
a583efd9b9 Give clippy the best day of his life 2024-07-12 22:34:42 +02:00
Remco Smits
6237c29a42 Merge branch 'main' into debugger 2024-07-12 22:21:46 +02:00
Remco Smits
9ea9b41e73 Merge main into debugger
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-12 22:18:39 +02:00
Remco Smits
8d99f9b7d2 Fix send all breakpoints when toggling breakpoints
We now send all the breakpoints for the current buffer when toggling one breakpoint.

Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-12 22:00:58 +02:00
Remco Smits
014ffbce2e Send the initial breakpoints from the project
We now send the initial breakpoints that we created before the debug adapter was running.

Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-12 21:11:38 +02:00
Remco Smits
68dd3c90c2 Fix we never created the first pane OOPS
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-12 21:10:39 +02:00
Remco Smits
4a6f6151f0 Remove editor highlights when debugger terminated event was received 2024-07-11 20:59:02 +02:00
Remco Smits
f108d4c705 Replace find method with any
Because we don't use the result of the find so any would be better in this case
2024-07-11 20:54:41 +02:00
Remco Smits
5ab95f1e1a Remove unused method 2024-07-11 20:51:56 +02:00
Remco Smits
49da08ffa4 Make goto stack trace frame work again after stopped event 2024-07-11 20:44:55 +02:00
Remco Smits
f287c897a4 Remove commented out code 2024-07-11 19:13:48 +02:00
Remco Smits
d15ff2d06f Don't spawn task for empty tasks vec 2024-07-11 19:13:26 +02:00
Remco Smits
648daa3237 WIP start adding support for debugging multiple threads at the same time
Start showing all the threads as tabs so you can debug multiple threads at the same time.
2024-07-10 22:31:49 +02:00
Remco Smits
33e127de09 Merge pull request #4 from Anthony-Eid/debugger
Clean up debugger Inventory code
2024-07-09 16:25:14 +02:00
Anthony Eid
5a9b279039 Merge with remco-debugger branch 2024-07-08 19:55:11 -04:00
Remco Smits
cce58570dc Remove unneeded clone 2024-07-07 20:52:40 +02:00
Remco Smits
361bbec3a0 Make some more room to show stack trace 2024-07-07 20:37:33 +02:00
Remco Smits
a87409813c Fix that thread is not always known in stopped event
This fixes an issue that not all debuggers send the `thread` event and we assumed that we always know the thread inside the handling code of the `stopped` event.
2024-07-07 20:24:33 +02:00
Remco Smits
da84aa1ac2 Fix panic with line - 1 and column - 1 2024-07-07 20:21:38 +02:00
Remco Smits
817760688a Don't support removing current thread_id 2024-07-07 17:32:29 +02:00
Remco Smits
13e56010c1 Add status types for thread state so we can disable buttons on this status 2024-07-07 17:27:40 +02:00
Remco Smits
b827a35e44 Make clippy's day again with using async mutex 2024-07-05 19:26:12 +02:00
Remco Smits
9006e8fdff Make clippy's day 2024-07-05 19:19:00 +02:00
Remco Smits
ce8ec033f4 Fix clippy 2024-07-05 19:16:38 +02:00
Remco Smits
9678cc9bc3 Remove editor highlights 2024-07-05 19:15:16 +02:00
Remco Smits
e87c4ddadc Merge branch 'main' into debugger 2024-07-05 17:02:58 +02:00
Remco Smits
3f8581a2fb Merge branch 'main' into debugger 2024-07-05 16:54:35 +02:00
Remco Smits
00c5b83384 Update handle terminated event to restart debugger 2024-07-05 16:50:43 +02:00
Remco Smits
4aedc1cd0b Save request type on client 2024-07-05 16:44:27 +02:00
Remco Smits
d238675c1a Wip support for determing to use launch or attach request 2024-07-05 16:31:07 +02:00
Remco Smits
dcf6f6ca30 Implement handle terminated event 2024-07-05 15:50:11 +02:00
Remco Smits
f4606bd951 Don't go to stack frame if event is not ment for current client 2024-07-05 15:11:26 +02:00
Remco Smits
12bef0830a Allow clicking on stack frames 2024-07-05 14:25:01 +02:00
Remco Smits
953a2b376c Make sure we don't resume other threads with action for one thread 2024-07-05 00:08:54 +02:00
Remco Smits
ac3b9f7a4c Wip show current file & column 2024-07-04 23:57:30 +02:00
Remco Smits
ef5990d427 Move ToggleBreakpoint action to editor instead of workspace 2024-07-04 14:52:50 +02:00
Remco Smits
23a81d5d70 Send all stack frame, scopes, and variable requests parallel
This will improve performance for loading large stack frames.
2024-07-04 13:58:12 +02:00
Remco Smits
515122c54d Clean up debug panel code
For now we only support debugger to be positioned on the bottom.
2024-07-03 23:20:39 +02:00
Remco Smits
f4eacca987 Fix 2 TODO's for multiple client support 2024-07-03 15:48:47 +02:00
Remco Smits
003fb7c81e Move thread state to the client
This allows us to show the current line and is closer for multi client/adapter support
2024-07-03 14:53:22 +02:00
Remco Smits
7d2f63ebbd Split event handling code to separate functions 2024-07-03 12:49:49 +02:00
Remco Smits
93e0bbb833 Use parking lot mutex instead of sync mutex 2024-07-03 10:40:02 +02:00
Remco Smits
8c5f6a0be7 Make clippppy more happy 2024-07-03 10:12:07 +02:00
Remco Smits
d6cafb8315 Fix typo 2024-07-03 09:45:54 +02:00
Remco Smits
a24b76b30b Merge branch 'main' into debugger 2024-07-03 09:38:56 +02:00
Remco Smits
11d74ea4ec Add tooltip for stack trace debug items
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-02 19:00:35 +02:00
Remco Smits
93f4775cf6 Properly fix race condition of receive events and handling responses
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-07-02 19:00:00 +02:00
Remco Smits
a93913b9a3 Swap handle events with handle request
This is a temporary fix, so we don't crash when race condition occurs and the event is send earlier than request was finished. The debug adapter was still `starting` meaning the task was still pending.
2024-07-02 16:51:30 +02:00
Remco Smits
08afbc6b58 Update capabilities 2024-07-02 15:09:44 +02:00
Remco Smits
b869465f00 Fix send configuration done request when initialized event was received.
This fixes an issue that the debugger adapter was notified to early. So it had in correct breakpoint information and would always stop on the first line without given a stack trace
2024-07-02 15:09:26 +02:00
Remco Smits
2debea8115 Wire through transport type from config 2024-07-02 15:06:09 +02:00
Remco Smits
cae295ff65 Fix send correct line to set breakpoint 2024-07-02 15:02:59 +02:00
Remco Smits
c51206e980 Wire through launch config values to launch request 2024-07-02 10:59:02 +02:00
Remco Smits
1baa5aea94 Fix missing match arg for Initialized event 2024-07-02 10:57:49 +02:00
Remco Smits
3e022a5565 Change error message when non-success response came back 2024-07-02 10:46:52 +02:00
Remco Smits
3dd769be94 Fix panic denormalizing Terminated event without body 2024-07-02 10:46:12 +02:00
Remco Smits
7936a4bee3 Fix panic when denormalizing Initialized event with body 2024-07-02 10:45:41 +02:00
Remco Smits
09aabe481c Undo wrong request value 2024-07-01 16:09:06 +02:00
Remco Smits
2ea1e4fa85 Add debug panel actions 2024-07-01 12:45:08 +02:00
Remco Smits
8699dad0e3 Add restart & pause method & log request errors 2024-07-01 12:43:57 +02:00
Remco Smits
47a5f0c620 Start wiring through custom launch data 2024-06-29 16:47:51 +02:00
Remco Smits
854ff68bac Clean up transport code 2024-06-29 16:38:43 +02:00
Remco Smits
01d384e676 Add missing event body mapping 2024-06-29 16:36:32 +02:00
Anthony Eid
ddd893a795 Clean up debugger inventory files/code 2024-06-28 00:52:15 -04:00
Remco Smits
3d7cd5dac7 Remove todo and change casing 2024-06-27 22:00:01 +02:00
Remco Smits
a6fdfb5191 Merge pull request #3 from Anthony-Eid/debugger
Debugger Tasks
2024-06-27 20:35:10 +02:00
Anthony Eid
73a68d560f Change abs_path to path in project update worktree settings 2024-06-27 14:31:50 -04:00
Anthony Eid
b4eeb25f55 Merge branch 'remco-debugger' into debugger 2024-06-27 14:15:03 -04:00
Remco Smits
fc991ab273 Merge branch 'main' into debugger 2024-06-27 20:12:43 +02:00
Remco Smits
ab58d14559 Wip display scopes & variables 2024-06-27 20:05:02 +02:00
Anthony Eid
3a0b311378 Get start debugger task to run DAP on confirm 2024-06-27 13:23:31 -04:00
Anthony Eid
b81065fe63 Get debug tasks to show in start debugger menu 2024-06-27 13:23:31 -04:00
Anthony Eid
a67f28dba2 Get Start Debugger action to open task menu 2024-06-27 13:23:31 -04:00
Remco Smits
99b2472e83 Merge pull request #2 from d1y/debugger
debug add icon
2024-06-27 18:40:37 +02:00
Remco Smits
be45d5aa73 Merge branch 'debugger' into debugger 2024-06-27 18:24:00 +02:00
Remco Smits
0508df9e7b Remove commit hash use repo url only 2024-06-26 18:20:18 +02:00
Remco Smits
153efab377 Migrate to dap-types crate 2024-06-26 17:58:48 +02:00
Remco Smits
8015fb70e3 Remove unused mut self 2024-06-26 11:44:43 +02:00
Remco Smits
79d23aa4fe Start adding support for multiple threads + beginning of stack frame UI 2024-06-25 21:38:01 +02:00
Remco Smits
d5dae425fc Allow stepping to next line 2024-06-25 21:35:47 +02:00
d1y
d9e09c4a66 add debug icon 2024-06-26 03:31:19 +08:00
Remco Smits
331625e876 Change hardcoded set breakpoint to TODO 2024-06-25 21:07:01 +02:00
Remco Smits
61949fb348 Add enum for SteppingGranularity 2024-06-25 21:06:33 +02:00
Remco Smits
c7f4e09496 Merge pull request #1 from Anthony-Eid/debugger
Set up ground work for debugger config files
2024-06-25 09:27:58 +02:00
Anthony Eid
5442e116ce Get debugger inventory to add configs from .zed/debug.json 2024-06-25 02:45:46 -04:00
Anthony Eid
11a4fc8b02 Start work on debugger inventory
The debugger config inventory is used to track config files for the debugger. Currently, only .zed/debug.json is set up to work, but it's still untested.
2024-06-25 02:13:52 -04:00
Remco Smits
d303ebd46e Wip add select launch config modal
Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2024-06-24 21:00:09 +02:00
Remco Smits
e1de8dc50e Remove unused code 2024-06-24 18:47:17 +02:00
Remco Smits
5fe110c1dd Rename play to continue debug 2024-06-24 00:07:43 +02:00
Remco Smits
89b203d03a Move current thread to debug panel instead of client 2024-06-23 23:52:10 +02:00
Remco Smits
0f4f8abbaa Add start debugger command 2024-06-23 23:51:53 +02:00
Remco Smits
0d97e9e579 Send debug adapter event through project and listen in debug panel 2024-06-23 14:27:07 +02:00
Remco Smits
14b913fb4b Make request return result of struct instead of Value 2024-06-23 11:21:15 +02:00
Remco Smits
9f1cd2bdb5 Merge branch 'main' into debugger 2024-06-23 10:52:52 +02:00
Remco Smits
547c40e332 change duration 2024-06-23 10:42:34 +02:00
Remco Smits
9cff6d5aa5 Wire through project path
Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-06-23 01:53:41 +02:00
Remco Smits
7e438bc1f3 Wip breakpoints
Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-06-23 00:36:31 +02:00
Remco Smits
944a52ce91 Wip break points
Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-06-22 23:47:18 +02:00
Remco Smits
95a814ed41 Wip breakpoints
Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-06-22 22:55:39 +02:00
Remco Smits
6b9295b6c4 wip write through buttons
Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-06-22 21:17:43 +02:00
Remco Smits
1128fce61a make thread id public 2024-06-22 20:22:32 +02:00
Remco Smits
7c355fdb0f clean up 2024-06-22 20:22:23 +02:00
Remco Smits
0e2a0b9edc Remove deps 2024-06-22 20:22:12 +02:00
Remco Smits
c130f9c2f2 Wip 2024-06-10 21:41:36 +02:00
Remco Smits
08300c6e90 Wip debugger client 2024-06-10 20:38:00 +02:00
Remco Smits
7b71119094 Move to imports 2024-06-09 13:01:46 +02:00
Remco Smits
c18db76862 Merge main 2024-06-09 12:27:47 +02:00
Remco Smits
c0dd152509 Merge branch 'main' into debugger 2024-06-09 12:27:39 +02:00
Remco Smits
f402a4e5ce WIP 2024-04-10 15:57:29 +02:00
839 changed files with 25615 additions and 52573 deletions

View File

@@ -12,7 +12,3 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
[target.'cfg(target_os = "windows")']
rustflags = ["--cfg", "windows_slim_errors"]

View File

@@ -3,15 +3,6 @@ export default {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
// These pages were removed, but may still be served due to Cloudflare's
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
if (
url.pathname === "/docs/assistant/context-servers" ||
url.pathname === "/docs/assistant/model-context-protocol"
) {
return await fetch("https://zed.dev/404");
}
let res = await fetch(url, request);
if (res.status === 404) {

View File

@@ -1,15 +1,13 @@
Closes #ISSUE
Release Notes:
- Added/Fixed/Improved ...
- Added/Fixed/Improved ... ([#NNNNN](https://github.com/zed-industries/zed/issues/NNNNN)).
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
### Or...
Closes #ISSUE
Release Notes:
- N/A

View File

@@ -147,8 +147,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo clippy
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
run: ./script/clippy
- name: Build Zed
run: cargo build -p zed
@@ -167,7 +166,6 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
@@ -232,20 +230,20 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -277,7 +275,6 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -321,7 +318,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -348,7 +345,6 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -363,7 +359,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
with:
mold-version: 2.32.0
@@ -406,7 +402,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: pnpm/action-setup@v3
with:
version: 9

View File

@@ -106,12 +106,10 @@ jobs:
export ZED_KUBE_NAMESPACE=production
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=2
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
export ZED_KUBE_NAMESPACE=staging
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=1
else
echo "cowardly refusing to deploy from an unknown branch"
exit 1
@@ -136,9 +134,3 @@ jobs:
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
export ZED_SERVICE_NAME=llm
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_LLM_LOAD_BALANCER_SIZE_UNIT
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"

View File

@@ -1,24 +0,0 @@
name: Docs
on:
pull_request:
paths:
- "docs/**"
push:
branches:
- main
jobs:
check_formatting:
name: "Check formatting"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- run: pnpm dlx prettier . --check
working-directory: ./docs

View File

@@ -16,7 +16,7 @@ jobs:
fi
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@e6b5885fb83c81ca9a700a91b079baec2133be3e # v1.4.0
uses: 2428392/gh-truncate-string-action@67b1b814955634208b103cff064be3cb1c7a19be # v1.3.0
id: get-content
with:
stringToTruncate: |

View File

@@ -67,7 +67,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Install Node
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
@@ -107,7 +106,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -141,7 +139,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -157,7 +154,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
with:
mold-version: 2.32.0

4
.gitignore vendored
View File

@@ -29,6 +29,4 @@ DerivedData/
.vscode
.wrangler
.flatpak-builder
# Don't commit any secrets to the repo.
.env.secret.toml
.zed/debug.json

View File

@@ -24,8 +24,7 @@ Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com> <146845123+evrsen@users.noreply.github.com>
Evren Sen <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>

View File

@@ -26,10 +26,6 @@
"tab_size": 2,
"formatter": "prettier"
},
"CSS": {
"tab_size": 2,
"formatter": "prettier"
},
"Rust": {
"tasks": {
"variables": {

View File

@@ -3,10 +3,5 @@
"label": "clippy",
"command": "./script/clippy",
"args": []
},
{
"label": "cargo run --profile release-fast",
"command": "cargo",
"args": ["run", "--profile", "release-fast"]
}
]

3923
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
[workspace]
resolver = "2"
members = [
"crates/activity_indicator",
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_slash_command",
"crates/assistant_tooling",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -19,8 +19,10 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/context_servers",
"crates/completion",
"crates/copilot",
"crates/dap",
"crates/debugger_ui",
"crates/db",
"crates/dev_server_projects",
"crates/diagnostics",
@@ -70,7 +72,6 @@ members = [
"crates/outline",
"crates/outline_panel",
"crates/paths",
"crates/performance",
"crates/picker",
"crates/prettier",
"crates/project",
@@ -126,10 +127,6 @@ members = [
"crates/zed",
"crates/zed_actions",
#
# Extensions
#
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
@@ -146,12 +143,10 @@ members = [
"extensions/lua",
"extensions/ocaml",
"extensions/php",
"extensions/perplexity",
"extensions/prisma",
"extensions/purescript",
"extensions/ruff",
"extensions/ruby",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/svelte",
"extensions/terraform",
@@ -161,25 +156,20 @@ members = [
"extensions/vue",
"extensions/zig",
#
# Tooling
#
"tooling/xtask",
]
default-members = ["crates/zed"]
resolver = "2"
[workspace.dependencies]
#
# Workspace member crates
#
activity_indicator = { path = "crates/activity_indicator" }
aho-corasick = "1.1"
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tooling = { path = "crates/assistant_tooling" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
breadcrumbs = { path = "crates/breadcrumbs" }
@@ -193,9 +183,11 @@ collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
context_servers = { path = "crates/context_servers" }
completion = { path = "crates/completion" }
copilot = { path = "crates/copilot" }
dap = { path = "crates/dap" }
db = { path = "crates/db" }
debugger_ui = { path = "crates/debugger_ui" }
dev_server_projects = { path = "crates/dev_server_projects" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
@@ -214,7 +206,6 @@ go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
handlebars = "4.3"
headless = { path = "crates/headless" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
@@ -243,7 +234,6 @@ open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
paths = { path = "crates/paths" }
performance = { path = "crates/performance" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -254,7 +244,6 @@ project_symbols = { path = "crates/project_symbols" }
proto = { path = "crates/proto" }
quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
@@ -300,44 +289,39 @@ worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
#
# External crates
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "cacdb5bb3b72bad2c729227537979d95af75978f" }
alacritty_terminal = "0.23"
any_vec = "0.14"
anyhow = "1.0.86"
ashpd = "0.9.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
async-dispatcher = { version = "0.1" }
async-fs = "1.6"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
async-tungstenite = "0.23"
async-tungstenite = { version = "0.16" }
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
base64 = "0.13"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-util = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
cargo_metadata = "0.18"
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
clickhouse = { version = "0.11.6" }
cocoa = "0.25"
core-foundation = "0.9.3"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
ctor = "0.2.6"
dashmap = "6.0"
dashmap = "5.5.3"
derive_more = "0.99.17"
dirs = "4.0"
emojis = "0.6.1"
env_logger = "0.11"
env_logger = "0.10"
exec = "0.3.1"
fork = "0.1.23"
futures = "0.3"
@@ -347,18 +331,17 @@ git2 = { version = "0.19", default-features = false }
globset = "0.4"
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
hyper = "0.14"
html5ever = "0.27.0"
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
] }
itertools = "0.11.0"
jsonwebtoken = "9.3"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
@@ -379,10 +362,11 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.10.0", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
rsa = "0.9.6"
runtimelib = { version = "0.15", default-features = false, features = [
runtimelib = { version = "0.12", default-features = false, features = [
"async-dispatcher-runtime",
] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
@@ -406,10 +390,8 @@ similar = "1.3"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strsim = "0.11"
strum = { version = "0.25.0", features = ["derive"] }
subtle = "2.5.0"
sys-locale = "0.3.1"
sysinfo = "0.30.7"
tempfile = "3.9.0"
thiserror = "1.0.29"
@@ -433,7 +415,7 @@ tree-sitter-css = "0.21"
tree-sitter-elixir = "0.2"
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = "0.21"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "1f55029bacd0a6a11f6eb894c4312d429dcf735c", package = "tree-sitter-gomod" }
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod"}
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work", rev = "dcbabff454703c3a4bc98a23cf8778d4be46fd22" }
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "6dd0303acf7138dd2b9b432a229e16539581c701" }
tree-sitter-html = "0.20"
@@ -454,32 +436,20 @@ url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "21.0.1", default-features = false, features = [
wasmtime = { version = "19.0.2", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
] }
wasmtime-wasi = "21.0.1"
wasmtime-wasi = "19.0.2"
which = "6.0.0"
wit-component = "0.201"
[workspace.dependencies.async-stripe]
version = "0.38"
default-features = false
features = [
"runtime-tokio-hyper-rustls",
"billing",
"checkout",
"events",
# The features below are only enabled to get the `events` feature to build.
"chrono",
"connect",
]
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.58"
version = "0.57"
features = [
"implement",
"Foundation_Numerics",
@@ -499,6 +469,7 @@ features = [
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Storage_FileSystem",
"Win32_System_LibraryLoader",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_DataExchange",
@@ -517,10 +488,6 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
[patch.crates-io]
# Patch Tree-sitter for updated wasmtime.
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f4a57817d58a2f134fe863674acad6bbf007228" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
@@ -572,6 +539,13 @@ single_range_in_vec_init = "allow"
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
almost_complete_range = "allow"
arc_with_non_send_sync = "allow"
borrowed_box = "allow"
let_underscore_future = "allow"
map_entry = "allow"
non_canonical_partial_ord_impl = "allow"
reversed_empty_ranges = "allow"
type_complexity = "allow"
[workspace.metadata.cargo-machete]

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.80-bookworm as builder
FROM rust:1.79-bookworm as builder
WORKDIR app
COPY . .
@@ -27,7 +27,5 @@ RUN apt-get update; \
WORKDIR app
COPY --from=builder /app/collab /app/collab
COPY --from=builder /app/crates/collab/migrations /app/migrations
COPY --from=builder /app/crates/collab/migrations_llm /app/migrations_llm
ENV MIGRATIONS_PATH=/app/migrations
ENV LLM_DATABASE_MIGRATIONS_PATH=/app/migrations_llm
ENTRYPOINT ["/app/collab"]

View File

@@ -1,3 +1,3 @@
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve
livekit: livekit-server --dev
blob_store: ./script/run-local-minio

View File

@@ -1,2 +0,0 @@
app: postgrest crates/collab/postgrest_app.conf
llm: postgrest crates/collab/postgrest_llm.conf

View File

@@ -1,11 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1896_18)">
<path d="M11.094 3.09999H8.952L12.858 12.9H15L11.094 3.09999Z" fill="#1F1F1E"/>
<path d="M4.906 3.09999L1 12.9H3.184L3.98284 10.842H8.06915L8.868 12.9H11.052L7.146 3.09999H4.906ZM4.68928 9.02199L6.026 5.57799L7.3627 9.02199H4.68928Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1896_18">
<rect width="14" height="9.8" fill="white" transform="translate(1 3.09999)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 530 B

View File

@@ -1,12 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
<g clip-path="url(#clip0_1916_18)">
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1916_18">
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8695 8.14262C13.8695 11.6221 11.4867 14.0984 7.96785 14.0984C4.59408 14.0984 1.86949 11.3738 1.86949 7.99999C1.86949 4.62622 4.59408 1.90163 7.96785 1.90163C9.61048 1.90163 10.9924 2.50409 12.0572 3.49754L10.3974 5.09344C8.22605 2.99836 4.18834 4.57213 4.18834 7.99999C4.18834 10.127 5.88752 11.8508 7.96785 11.8508C10.3826 11.8508 11.2875 10.1197 11.4301 9.22213H7.96785V7.12458H13.7736C13.8301 7.43688 13.8695 7.73688 13.8695 8.14262Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 569 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0768 6.72994C14.3987 5.77663 14.2879 4.73232 13.7731 3.86519C12.9989 2.53519 11.4427 1.85094 9.92272 2.17294C9.24656 1.42132 8.2751 0.993879 7.25664 1C5.70301 0.996504 4.32452 1.9835 3.84655 3.44213C2.84849 3.64382 1.98699 4.26025 1.48286 5.13394C0.70294 6.46044 0.880738 8.13257 1.9227 9.27007C1.6008 10.2234 1.71164 11.2677 2.22642 12.1348C3.00057 13.4648 4.55686 14.1491 6.07679 13.8271C6.75251 14.5787 7.72441 15.0061 8.74287 14.9996C10.2974 15.0035 11.6763 14.0156 12.1543 12.5557C13.1524 12.354 14.0139 11.7376 14.518 10.8639C15.297 9.53738 15.1188 7.86657 14.0773 6.72907L14.0768 6.72994ZM8.74376 14.0848C8.12169 14.0856 7.51912 13.8708 7.0416 13.4775C7.06332 13.4661 7.10101 13.4456 7.1254 13.4307L9.95066 11.8207C10.0952 11.7398 10.1839 11.5879 10.183 11.4239V7.49382L11.377 8.17413C11.3899 8.18025 11.3983 8.1925 11.4001 8.2065V11.4611C11.3983 12.9083 10.2105 14.0817 8.74376 14.0848ZM3.03116 11.6772C2.71946 11.1461 2.60729 10.5235 2.71414 9.91932C2.73498 9.93157 2.77178 9.95388 2.79794 9.96875L5.6232 11.5788C5.76642 11.6614 5.94377 11.6614 6.08743 11.5788L9.53654 9.6135V10.9741C9.53742 10.9881 9.53077 11.0017 9.51969 11.0104L6.66383 12.6375C5.39175 13.3603 3.76719 12.9306 3.03161 11.6772H3.03116ZM2.2876 5.592C2.59797 5.06 3.08792 4.65313 3.67141 4.44182C3.67141 4.46588 3.67008 4.50832 3.67008 4.53807V7.7585C3.6692 7.92213 3.75787 8.07394 3.90198 8.15488L7.35108 10.1197L6.15704 10.8C6.14507 10.8079 6.12999 10.8092 6.11669 10.8035L3.26039 9.17513C1.99098 8.44975 1.55557 6.84719 2.28716 5.59244L2.2876 5.592ZM12.098 7.84469L8.64887 5.87944L9.84292 5.19957C9.85489 5.19169 9.86996 5.19038 9.88326 5.19607L12.7396 6.82313C14.0112 7.54807 14.447 9.15325 13.7124 10.408C13.4015 10.9391 12.912 11.346 12.329 11.5578V8.24107C12.3303 8.07744 12.2421 7.92607 12.0984 7.84469H12.098ZM13.2863 6.07982C13.2654 6.06713 13.2286 6.04525 13.2025 6.03038L10.3772 4.42038C10.234 4.33769 10.0566 4.33769 9.91297 4.42038L6.46386 6.38563V5.025C6.46298 5.011 6.46963 4.99744 6.48071 4.98869L9.33657 3.36294C10.6086 2.63888 12.235 3.06982 12.9683 4.32544C13.2783 4.85569 13.3905 5.4765 13.2854 6.07982H13.2863ZM5.81475 8.50488L4.62026 7.82457C4.6074 7.81844 4.59898 7.80619 4.59721 7.79219V4.53763C4.59809 3.08863 5.78947 1.91438 7.25797 1.91525C7.87916 1.91525 8.48039 2.1305 8.95792 2.5225C8.93619 2.53388 8.89894 2.55444 8.87412 2.56932L6.04885 4.17932C5.90431 4.26025 5.81563 4.41163 5.81652 4.57569L5.81475 8.504V8.50488ZM6.46342 7.125L7.99976 6.24957L9.53609 7.12457V8.875L7.99976 9.75L6.46342 8.875V7.125Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1882_101)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3125 1.875C2.07088 1.875 1.875 2.07088 1.875 2.3125V11.9375H1V2.3125C1 1.58763 1.58763 1 2.3125 1H14.0344C14.6191 1 14.9118 1.70688 14.4984 2.12029L7.27887 9.33984H9.3125V8.4375H10.1875V9.55859C10.1875 9.92103 9.89369 10.2148 9.53125 10.2148H6.40387L4.89996 11.7187H11.7187V6.25H12.5937V11.7187C12.5937 12.202 12.202 12.5937 11.7187 12.5937H4.02496L2.49371 14.125H13.6875C13.9291 14.125 14.125 13.9291 14.125 13.6875V4.0625H15V13.6875C15 14.4124 14.4124 15 13.6875 15H1.96561C1.38095 15 1.08816 14.2931 1.50157 13.8797L8.69379 6.6875H6.6875V7.5625H5.8125V6.46875C5.8125 6.10631 6.10631 5.8125 6.46875 5.8125H9.56879L11.1 4.28125H4.28125V9.75H3.40625V4.28125C3.40625 3.798 3.798 3.40625 4.28125 3.40625H11.975L13.5063 1.875H2.3125Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1882_101">
<rect width="14" height="14" fill="white" transform="translate(1 1)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<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-database-zap"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 15 21.84"/><path d="M21 5V8"/><path d="M21 12L18 17H22L19 22"/><path d="M3 12A9 3 0 0 0 14.59 14.87"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 2H4v12H2.5V2zm4.936.39L6.25 3v10l1.186.61 7-5V7.39l-7-5zM12.71 8l-4.96 3.543V4.457L12.71 8z"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M4.5 3H6v10H4.5V3zm7 0v10H10V3h1.5z"/></svg>

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 8a4.5 4.5 0 0 1-8.61 1.834l-1.391.565A6.001 6.001 0 0 0 14.25 8 6 6 0 0 0 3.5 4.334V2.5H2v4l.75.75h3.5v-1.5H4.352A4.5 4.5 0 0 1 12.75 8z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 9.532h.542l3.905-3.905-1.061-1.06-2.637 2.61V1H7.251v6.177l-2.637-2.61-1.061 1.06 3.905 3.905H8zm1.956 3.481a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1h-.542L3.553 4.905l1.061 1.06 2.637-2.61v6.177h1.498V3.355l2.637 2.61 1.061-1.06L8.542 1H8zm1.956 12.013a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 5.75v-4h-1.5v2.542c-1.145-1.359-2.911-2.209-4.84-2.209-3.177 0-5.92 2.307-6.16 5.398l-.02.269h1.501l.022-.226c.212-2.195 2.202-3.94 4.656-3.94 1.736 0 3.244.875 4.05 2.166h-2.83v1.5h4.163l.962-.975V5.75h-.004zM8 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 1.99976L14 2.99976V12.9998L13 13.9998H3L2 12.9998L2 2.99976L3 1.99976H13ZM12.7461 3.25057L3.25469 3.25057L3.25469 12.7504H12.7461V3.25057Z"/></svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -1 +1,10 @@
<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-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_430_1270)">
<path d="M4.30957 0.857736V4.28922L2.35703 4.28788C2.10067 4.28788 1.86816 4.44057 1.76636 4.67656C1.66511 4.91229 1.71332 5.18633 1.88959 5.37304L5.53269 9.23312C5.77565 9.49028 6.22488 9.49028 6.46784 9.23312L10.1123 5.37277C10.2875 5.18794 10.3354 4.9147 10.2342 4.6763C10.1337 4.44057 9.90066 4.28788 9.66761 4.28788H7.73891L7.73891 0.857736C7.73891 0.383865 7.35504 2.35669e-07 6.88171 2.14979e-07L5.16731 1.4004e-07C4.66906 -0.000267757 4.30957 0.383865 4.30957 0.857736ZM11.1433 11.1187C11.1434 10.6687 10.7595 10.2856 10.2861 10.2856H1.71413C1.23972 10.2856 0.856659 10.6687 0.856659 11.1187C0.856659 11.6169 1.23972 12 1.71386 12H10.2861C10.7595 12 11.1433 11.6169 11.1433 11.1187Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_430_1270">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 954 B

View File

@@ -1 +0,0 @@
<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-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>

Before

Width:  |  Height:  |  Size: 320 B

View File

@@ -1 +0,0 @@
<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-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>

Before

Width:  |  Height:  |  Size: 358 B

View File

@@ -1 +0,0 @@
<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-code"><path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/></svg>

Before

Width:  |  Height:  |  Size: 388 B

View File

@@ -1 +0,0 @@
<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-text"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>

Before

Width:  |  Height:  |  Size: 384 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13L7.01562 8.98438" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8.6875 7.3125L9.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 5V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 5V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 10V8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M6 4L8 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 4L13 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 9L13 9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -1 +0,0 @@
<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-microscope"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2"/><path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"/><path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/></svg>

Before

Width:  |  Height:  |  Size: 418 B

View File

@@ -1 +0,0 @@
<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-route"><circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@@ -1 +0,0 @@
<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-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@@ -1 +0,0 @@
<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-slash"><path d="M22 2 2 22"/></svg>

Before

Width:  |  Height:  |  Size: 238 B

View File

@@ -1 +0,0 @@
<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-slash"><rect width="18" height="18" x="3" y="3" rx="2"/><line x1="9" x2="15" y1="15" y2="9"/></svg>

Before

Width:  |  Height:  |  Size: 309 B

View File

@@ -1,6 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

View File

@@ -1 +0,0 @@
<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-text-select"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/><line x1="7" x2="15" y1="8" y2="8"/><line x1="7" x2="17" y1="12" y2="12"/><line x1="7" x2="13" y1="16" y2="16"/></svg>

Before

Width:  |  Height:  |  Size: 610 B

View File

@@ -1 +0,0 @@
<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-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>

Before

Width:  |  Height:  |  Size: 288 B

View File

@@ -2,11 +2,13 @@
// Standard Linux bindings
{
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
@@ -31,20 +33,6 @@
"f11": "zed::ToggleFullScreen"
}
},
{
"context": "Picker || menu",
"bindings": {
"up": "menu::SelectPrev",
"down": "menu::SelectNext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrev",
"right": "menu::SelectNext"
}
},
{
"context": "Editor",
"bindings": {
@@ -52,6 +40,7 @@
"backspace": "editor::Backspace",
"shift-backspace": "editor::Backspace",
"delete": "editor::Delete",
"ctrl-d": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
@@ -133,7 +122,7 @@
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
"alt-right": "editor::AcceptPartialInlineCompletion"
}
},
{
@@ -161,9 +150,7 @@
"bindings": {
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary"
"alt-m": "assistant::ToggleModelSelector"
}
},
{
@@ -258,6 +245,13 @@
"ctrl-alt-shift-x": "search::ToggleRegex"
}
},
{
"context": "Terminal",
"bindings": {
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
}
},
// Bindings from VS Code
{
"context": "Editor",
@@ -275,7 +269,6 @@
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
@@ -437,7 +430,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
"tab": "editor::ConfirmCompletion"
}
},
{
@@ -467,16 +460,12 @@
{
"bindings": {
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
"ctrl-shift-c": "collab_panel::ToggleFocus",
"ctrl-alt-i": "zed::DebugElements",
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "!Terminal",
"bindings": {
"ctrl-shift-c": "collab_panel::ToggleFocus"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -491,8 +480,7 @@
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
"ctrl-shift-enter": "repl::Run"
}
},
{
@@ -611,14 +599,12 @@
"context": "Terminal",
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"ctrl-shift-c": "terminal::Copy",
"shift-ctrl-c": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
// "ctrl-a": "editor::SelectAll", // conflicts with readline
"ctrl-shift-v": "terminal::Paste",
"ctrl-a": "editor::SelectAll",
"shift-ctrl-v": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"up": ["terminal::SendKeystroke", "up"],
"pageup": ["terminal::SendKeystroke", "pageup"],
"down": ["terminal::SendKeystroke", "down"],

View File

@@ -127,9 +127,7 @@
"cmd-'": "editor::ToggleHunkDiff",
"cmd-\"": "editor::ExpandAllHunkDiffs",
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
"ctrl-f12": "editor::GoToDeclaration",
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
"cmd-i": "editor::ShowSignatureHelp"
}
},
{
@@ -154,7 +152,7 @@
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"cmd-right": "editor::AcceptPartialInlineCompletion"
"alt-right": "editor::AcceptPartialInlineCompletion"
}
},
{
@@ -180,8 +178,7 @@
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
"ctrl-shift-enter": "repl::Run"
}
},
{
@@ -189,9 +186,7 @@
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary"
"alt-m": "assistant::ToggleModelSelector"
}
},
{
@@ -474,7 +469,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
"tab": "editor::ConfirmCompletion"
}
},
{

View File

@@ -3,7 +3,7 @@
// For information on binding keys, see the Zed
// documentation: https://zed.dev/docs/key-bindings
//
// To see the default key bindings run `zed: open default keymap`
// To see the default key bindings run `zed: Open Default Keymap`
// from the command palette.
[
{

View File

@@ -4,6 +4,7 @@
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
@@ -88,11 +89,9 @@
"g t": "pane::ActivateNextItem",
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToDeclaration",
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g shift-d": "editor::GoToTypeDefinition",
"g cmd-d": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenFile",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -177,19 +176,19 @@
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitVertical",
"ctrl-w ctrl-v": "pane::SplitVertical",
"ctrl-w s": "pane::SplitHorizontal",
"ctrl-w shift-s": "pane::SplitHorizontal",
"ctrl-w ctrl-s": "pane::SplitHorizontal",
"ctrl-w v": "pane::SplitLeft",
"ctrl-w ctrl-v": "pane::SplitLeft",
"ctrl-w s": "pane::SplitUp",
"ctrl-w shift-s": "pane::SplitUp",
"ctrl-w ctrl-s": "pane::SplitUp",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": "workspace::NewFileSplitHorizontal",
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w d": "editor::GoToDefinitionSplit",
"ctrl-w g d": "editor::GoToDefinitionSplit",
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
@@ -199,12 +198,17 @@
"ctrl-6": "pane::AlternateFile"
}
},
{
"context": "VimControl && VimCount",
"bindings": {
"0": ["vim::Number", 0]
}
},
{
"context": "vim_mode == normal",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
".": "vim::Repeat",
"c": ["vim::PushOperator", "Change"],
"shift-c": "vim::ChangeToEndOfLine",
@@ -252,17 +256,9 @@
"g c": ["vim::PushOperator", "ToggleComments"]
}
},
{
"context": "VimControl && VimCount",
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
}
},
{
"context": "vim_mode == visual",
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
"U": "vim::ConvertToUpperCase",
"o": "vim::OtherEnd",
@@ -320,7 +316,6 @@
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"ctrl-r": ["vim::PushOperator", "Register"]
}
},
@@ -330,7 +325,6 @@
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
"backspace": "vim::UndoReplace",
"tab": "vim::Tab",
"enter": "vim::Enter"
@@ -343,8 +337,7 @@
"enter": "vim::Enter",
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators",
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }]
"ctrl-[": "vim::ClearOperators"
}
},
{

View File

@@ -1,61 +0,0 @@
{{#if language_name}}
Here's a file of {{language_name}} that I'm going to ask you to make an edit to.
{{else}}
Here's a file of text that I'm going to ask you to make an edit to.
{{/if}}
{{#if is_insert}}
The point you'll need to insert at is marked with <insert_here></insert_here>.
{{else}}
The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.
{{/if}}
<document>
{{{document_content}}}
</document>
{{#if is_truncated}}
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
{{/if}}
{{#if is_insert}}
You can't replace {{content_type}}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.
Generate {{content_type}} based on the following prompt:
<prompt>
{{{user_prompt}}}
</prompt>
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
Immediately start with the following format with no remarks:
```
\{{INSERTED_CODE}}
```
{{else}}
Edit the section of {{content_type}} in <rewrite_this></rewrite_this> tags based on the following prompt:
<prompt>
{{{user_prompt}}}
</prompt>
{{#if rewrite_section}}
And here's the section to rewrite based on that prompt again for reference:
<rewrite_this>
{{{rewrite_section}}}
</rewrite_this>
{{/if}}
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
Immediately start with the following format with no remarks:
```
\{{REWRITTEN_CODE}}
```
{{/if}}

View File

@@ -1,302 +0,0 @@
<workflow>
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
named Zed, which is where they will have entered this prompt and will be seeing the response.
<instructions>
- Use the language of the file for code fence blocks unless otherwise specified.
- Include a code or file action in each step.
- Only put code in separate steps if it should either go in separate files, or in different (non-contiguous) places in the same file.
- Provide error handling and input validation where appropriate.
- Adapt explanations based on the user's perceived level of expertise.
- Include comments in code examples to enhance understanding.
- Consider more complex programming scenarios when relevant.
- Avoid using the terminal to perform filesystem operations such as creating, deleting, renaming, and editing files. Assume the user prefers to use Zed for these operations, unless the prompt suggests that the user is expecting terminal commands.
- When creating a new file, Zed will automatically create any necessary directories in that file's path. So when a step will create a new file in Zed, don't mention needing to create the directory that it will go in.
- Assume the user is performing these actions in the Zed code editor, so avoid redundancies like "In your code editor, ..." or "In Zed, ..."
- Be concise without leaving out important information.
</instructions>
Examples of user-assistant dialogs with step numbers and user follow-up requests:
<example language="rust">
<turn number="1">
<user>Create a Library struct with a vector of Book structs. Include methods to add books and find books by author. Use generics for the book ID.</user>
<assistant>Certainly! Let's create a Library struct with a vector of Book structs, including methods to add books and find books by author, using generics for the book ID:
<step>Define the Book and Library structs
```rust
use std::fmt::Display;
struct Book<T>
where
T: Display + Clone
{
id: T,
title: String,
author: String,
}
struct Library<T>
where
T: Display + Clone
{
books: Vec<Book<T>>,
}
```
</step>
<step>Implement methods for the Library struct
```rust
impl<T> Library<T>
where
T: Display + Clone
{
fn new() -> Self {
Library { books: Vec::new() }
}
fn add_book(&mut self, id: T, title: String, author: String) {
let book = Book { id, title, author };
self.books.push(book);
}
fn find_books_by_author(&self, author: &str) -> Vec<&Book<T>> {
self.books
.iter()
.filter(|book| book.author.to_lowercase() == author.to_lowercase())
.collect()
}
}
```
</step>
This code defines:
1. A generic Book struct with an ID of type T, title, and author
2. A Library struct containing a vector of Books
3. Methods to create a new Library, add books, and find books by author
The use of generics allows flexibility in the type of ID used for books.</assistant>
</turn>
<turn number="2">
<user>How can I use this Library struct in a main function?</user>
<assistant>Here's how you can use the Library struct in a main function:
<step>Create a main function to demonstrate Library usage
```rust
fn main() {
// Create a new Library with string IDs
let mut library = Library::new();
// Add some books
library.add_book(String::from("001"), String::from("The Rust Programming Language"), String::from("Steve Klabnik"));
library.add_book(String::from("002"), String::from("Programming Rust"), String::from("Jim Blandy"));
library.add_book(String::from("003"), String::from("Rust in Action"), String::from("Tim McNamara"));
// Find books by author
let author = "Steve Klabnik";
let books = library.find_books_by_author(author);
println!("Books by {}:", author);
for book in books {
println!("ID: {}, Title: {}", book.id, book.title);
}
}
```
</step>
This main function demonstrates:
1. Creating a new Library instance
2. Adding books with string IDs
3. Finding and displaying books by a specific author</assistant>
</turn>
<turn number="3">
<user>Can you add a method to remove a book by ID and handle potential errors?</user>
<assistant>Certainly! Let's add a method to remove a book by ID and handle potential errors:
<step>Add a remove_book method to the Library struct
```rust
use std::fmt::Display;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct BookNotFoundError;
impl Error for BookNotFoundError {}
impl fmt::Display for BookNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Book not found")
}
}
impl<T> Library<T>
where
T: Display + Clone + PartialEq
{
// ... (previous methods remain the same)
fn remove_book(&mut self, id: &T) -> Result<Book<T>, BookNotFoundError> {
if let Some(index) = self.books.iter().position(|book| &book.id == id) {
Ok(self.books.remove(index))
} else {
Err(BookNotFoundError)
}
}
}
```
</step>
<step>Update the main function to demonstrate book removal
```rust
fn main() {
// ... (previous code remains the same)
// Attempt to remove a book
let id_to_remove = String::from("002");
match library.remove_book(&id_to_remove) {
Ok(removed_book) => println!("Removed book: {} by {}", removed_book.title, removed_book.author),
Err(e) => println!("Error removing book: {}", e),
}
// Attempt to remove a non-existent book
let non_existent_id = String::from("999");
match library.remove_book(&non_existent_id) {
Ok(_) => println!("Book removed successfully"),
Err(e) => println!("Error removing book: {}", e),
}
// Display remaining books
println!("\nRemaining books:");
for book in &library.books {
println!("ID: {}, Title: {} by {}", book.id, book.title, book.author);
}
}
```
</step>
This update includes:
1. A new remove_book method that returns a Result type
2. A custom BookNotFoundError for error handling
3. Demonstration of successful book removal and error handling for non-existent books
4. Display of remaining books after removal attempts
The remove_book method uses the PartialEq trait to compare book IDs, allowing for flexible ID types while maintaining type safety.</assistant>
</turn>
</example>
<example language="go">
<turn number="1">
<user>Create a Person struct with name, age, and email fields, and a method to greet. Include error handling for age.</user>
<assistant>Certainly! Let's create a Person struct with the specified fields and a method to greet in Go:
<step>Define the Person struct and NewPerson function with error handling
```go
type Person struct {
Name string
Age int
Email string
}
func NewPerson(name string, age int, email string) (*Person, error) {
if age < 0 || age > 150 {
return nil, fmt.Errorf("invalid age: %d", age)
}
return &Person{Name: name, Age: age, Email: email}, nil
}
```
</step>
<step>Implement the Greet method for the Person struct
```go
func (p *Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s, %d years old. You can reach me at %s.", p.Name, p.Age, p.Email)
}
```
</step>
This code defines a Person struct, a NewPerson function with age validation, and a Greet method.</assistant>
</turn>
<turn number="2">
<user>How can I use these in a main function?</user>
<assistant>Here's how to use the Person struct and its methods in a main function:
<step>Create a main function with error handling
```go
func main() {
// Create a new Person instance
person, err := NewPerson("Alice", 30, "alice@example.com")
if err != nil {
fmt.Println("Error creating person:", err)
return
}
// Use the Greet method
fmt.Println(person.Greet())
// Demonstrate error handling with invalid age
invalidPerson, err := NewPerson("Bob", 200, "bob@example.com")
if err != nil {
fmt.Println("Error creating person with invalid age:", err)
}
}
```
</step>
This main function demonstrates:
1. Creating a valid Person instance
2. Using the Greet method
3. Handling errors when creating a Person with an invalid age</assistant>
</turn>
<turn number="3">
<user>Can you add a method to update the email with validation?</user>
<assistant>Certainly! Let's add a method to update the email with validation:
<step>Add UpdateEmail method with validation
```go
import (
"fmt"
"regexp"
)
func (p *Person) UpdateEmail(newEmail string) error {
// Simple email validation using regex
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(newEmail) {
return fmt.Errorf("invalid email format: %s", newEmail)
}
p.Email = newEmail
return nil
}
```
</step>
<step>Update the main function to demonstrate email update
```go
func main() {
// ... (previous code remains the same)
// Demonstrate email update
err = person.UpdateEmail("alice.new@example.com")
if err != nil {
fmt.Println("Error updating email:", err)
} else {
fmt.Println("Updated greeting:", person.Greet())
}
// Demonstrate email update with invalid email
err = person.UpdateEmail("invalid-email")
if err != nil {
fmt.Println("Error updating email:", err)
}
}
```
</step>
This update includes:
1. An UpdateEmail method with email format validation
2. Demonstration of successful email update in the main function
3. Handling of invalid email update attempt</assistant>
</turn>
</example>
</workflow>

View File

@@ -0,0 +1,241 @@
Your task is to map a step from the conversation above to operations on symbols inside the provided source files.
Guidelines:
- There's no need to describe *what* to do, just *where* to do it.
- If creating a file, assume any subsequent updates are included at the time of creation.
- Don't create and then update a file.
- We'll create it in one shot.
- Prefer updating symbols lower in the syntax tree if possible.
- Never include operations on a parent symbol and one of its children in the same <operations> block.
- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes.
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
- Descriptions are required for all operations except delete.
- When generating multiple operations, ensure the descriptions are specific to each individual operation.
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description.
The available operation types are:
1. <update>: Modify an existing symbol in a file.
2. <create_file>: Create a new file.
3. <insert_sibling_after>: Add a new symbol as sibling after an existing symbol in a file.
4. <append_child>: Add a new symbol as the last child of an existing symbol in a file.
5. <prepend_child>: Add a new symbol as the first child of an existing symbol in a file.
6. <delete>: Remove an existing symbol from a file. The `description` attribute is invalid for delete, but required for other ops.
All operations *require* a path.
Operations that *require* a symbol: <update>, <insert_sibling_after>, <delete>
Operations that don't allow a symbol: <create>
Operations that have an *optional* symbol: <prepend_child>, <append_child>
Example 1:
User:
```rs src/rectangle.rs
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
Symbols for src/rectangle.rs:
- struct Rectangle
- impl Rectangle
- impl Rectangle fn new
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
<step>Implement the 'Display' trait for the Rectangle struct</step>
What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
Assistant (wrong):
<operations>
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_area method" />
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_perimeter method" />
</operations>
This demonstrates what NOT to do. NEVER append multiple children at the same location.
Assistant (corrected):
<operations>
<append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate area and perimeter methods" />
</operations>
User:
What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
Assistant:
<operations>
<insert_sibling_after path="src/shapes.rs" symbol="impl Rectangle" description="Implement Display trait for Rectangle"/>
</operations>
Example 2:
User:
```rs src/user.rs
struct User {
pub name: String,
age: u32,
email: String,
}
impl User {
fn new(name: String, age: u32, email: String) -> Self {
User { name, age, email }
}
pub fn print_info(&self) {
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
}
}
```
Symbols for src/user.rs:
- struct User
- struct User pub name
- struct User age
- struct User email
- impl User
- impl User fn new
- impl User pub fn print_info
<step>Update the 'print_info' method to use formatted output</step>
<step>Remove the 'email' field from the User struct</step>
What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
Assistant:
<operations>
<update path="src/user.rs" symbol="impl User fn print_info" description="Use formatted output" />
</operations>
User:
What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
Assistant:
<operations>
<delete path="src/user.rs" symbol="struct User email" description="Remove the email field" />
</operations>
Example 3:
User:
```rs src/vehicle.rs
struct Vehicle {
make: String,
model: String,
year: u32,
}
impl Vehicle {
fn new(make: String, model: String, year: u32) -> Self {
Vehicle { make, model, year }
}
fn print_year(&self) {
println!("Year: {}", self.year);
}
}
```
Symbols for src/vehicle.rs:
- struct Vehicle
- struct Vehicle make
- struct Vehicle model
- struct Vehicle year
- impl Vehicle
- impl Vehicle fn new
- impl Vehicle fn print_year
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
Assistant:
<operations>
<prepend_child path="src/vehicle.rs" description="Add 'use std::fmt' statement" />
</operations>
User:
What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
Assistant:
<operations>
<insert_sibling_after path="src/vehicle.rs" symbol="impl Vehicle fn new" description="Add start_engine method"/>
</operations>
Example 4:
User:
```rs src/employee.rs
struct Employee {
name: String,
position: String,
salary: u32,
department: String,
}
impl Employee {
fn new(name: String, position: String, salary: u32, department: String) -> Self {
Employee { name, position, salary, department }
}
fn print_details(&self) {
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
self.name, self.position, self.salary, self.department);
}
fn give_raise(&mut self, amount: u32) {
self.salary += amount;
}
}
```
Symbols for src/employee.rs:
- struct Employee
- struct Employee name
- struct Employee position
- struct Employee salary
- struct Employee department
- impl Employee
- impl Employee fn new
- impl Employee fn print_details
- impl Employee fn give_raise
<step>Make salary an f32</step>
What are the operations for the step: <step>Make salary an f32</step>
A (wrong):
<operations>
<update path="src/employee.rs" symbol="struct Employee" description="Change the type of salary to an f32" />
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
</operations>
This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
A (corrected):
<operations>
<update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
</operations>
User:
What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
A:
<operations>
<delete path="src/employee.rs" symbol="struct Employee department" />
<update path="src/employee.rs" symbol="impl Employee fn print_details" description="Don't print the 'department' field" />
</operations>
Now generate the operations for the following step.
Output only valid XML containing valid operations with their required attributes.
NEVER output code or any other text inside <operation> tags. If you do, you will replaced with another model.
Your response *MUST* begin with <operations> and end with </operations>:

View File

@@ -1,496 +0,0 @@
<overview>
Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
Given a workflow containing background context plus a series of <step> tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
With each location, you will produce a brief, one-line description of the changes to be made.
<guidelines>
- There's no need to describe *what* to do, just *where* to do it.
- Only reference locations that actually exist (unless you're creating a file).
- If creating a file, assume any subsequent updates are included at the time of creation.
- Don't create and then update a file. Always create new files in shot.
- Prefer updating symbols lower in the syntax tree if possible.
- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
- Descriptions are required for all suggestions except delete.
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
</guidelines>
</overview>
<examples>
<example>
<workflow_context>
<message role="user">
```rs src/rectangle.rs
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
</message>
<message role="assistant">
Sure, I can help with that!
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
<step>Implement the 'Display' trait for the Rectangle struct</step>
</message>
</workflow_context>
<step_to_resolve>
Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
</step_to_resolve>
<incorrect_output reason="NEVER append multiple children at the same location.">
{
"title": "Add Rectangle methods",
"suggestions": [
{
"kind": "AppendChild",
"path": "src/shapes.rs",
"symbol": "impl Rectangle",
"description": "Add calculate_area method"
},
{
"kind": "AppendChild",
"path": "src/shapes.rs",
"symbol": "impl Rectangle",
"description": "Add calculate_perimeter method"
}
]
}
</incorrect_output>
<correct_output>
{
"title": "Add Rectangle methods",
"suggestions": [
{
"kind": "AppendChild",
"path": "src/shapes.rs",
"symbol": "impl Rectangle",
"description": "Add calculate area and perimeter methods"
}
]
}
</correct_output>
<step_to_resolve>
Implement the 'Display' trait for the Rectangle struct
</step_to_resolve>
<output>
{
"title": "Implement Display for Rectangle",
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/shapes.rs",
"symbol": "impl Rectangle",
"description": "Implement Display trait for Rectangle"
}
]
}
</output>
<example>
<workflow_context>
<message role="user">
```rs src/user.rs
struct User {
pub name: String,
age: u32,
email: String,
}
impl User {
fn new(name: String, age: u32, email: String) -> Self {
User { name, age, email }
}
pub fn print_info(&self) {
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
}
}
```
</message>
<message role="assistant">
Certainly!
<step>Update the 'print_info' method to use formatted output</step>
<step>Remove the 'email' field from the User struct</step>
</message>
</workflow_context>
<step_to_resolve>
Update the 'print_info' method to use formatted output
</step_to_resolve>
<output>
{
"title": "Use formatted output",
"suggestions": [
{
"kind": "Update",
"path": "src/user.rs",
"symbol": "impl User pub fn print_info",
"description": "Use formatted output"
}
]
}
</output>
<step_to_resolve>
Remove the 'email' field from the User struct
</step_to_resolve>
<output>
{
"title": "Remove email field",
"suggestions": [
{
"kind": "Delete",
"path": "src/user.rs",
"symbol": "struct User email"
}
]
}
</output>
</example>
<example>
<workflow_context>
<message role="user">
```rs src/vehicle.rs
struct Vehicle {
make: String,
model: String,
year: u32,
}
impl Vehicle {
fn new(make: String, model: String, year: u32) -> Self {
Vehicle { make, model, year }
}
fn print_year(&self) {
println!("Year: {}", self.year);
}
}
```
</message>
<message role="assistant">
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
</message>
</workflow_context>
<step_to_resolve>
Add a 'use std::fmt;' statement at the beginning of the file
</step_to_resolve>
<output>
{
"title": "Add use std::fmt statement",
"suggestions": [
{
"kind": "PrependChild",
"path": "src/vehicle.rs",
"symbol": "#imports",
"description": "Add 'use std::fmt' statement"
}
]
}
</output>
<step_to_resolve>
Add a new method 'start_engine' in the Vehicle impl block
</step_to_resolve>
<output>
{
"title": "Add start_engine method",
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/vehicle.rs",
"symbol": "impl Vehicle fn new",
"description": "Add start_engine method"
}
]
}
</output>
</example>
<example>
<workflow_context>
<message role="user">
```rs src/employee.rs
struct Employee {
name: String,
position: String,
salary: u32,
department: String,
}
impl Employee {
fn new(name: String, position: String, salary: u32, department: String) -> Self {
Employee { name, position, salary, department }
}
fn print_details(&self) {
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
self.name, self.position, self.salary, self.department);
}
fn give_raise(&mut self, amount: u32) {
self.salary += amount;
}
}
```
</message>
<message role="assistant">
<step>Make salary an f32</step>
<step>Remove the 'department' field and update the 'print_details' method</step>
</message>
</workflow_context>
<step_to_resolve>
Make salary an f32
</step_to_resolve>
<incorrect_output reason="NEVER include suggestions on a parent symbol and one of its children in the same suggestions block.">
{
"title": "Change salary to f32",
"suggestions": [
{
"kind": "Update",
"path": "src/employee.rs",
"symbol": "struct Employee",
"description": "Change the type of salary to an f32"
},
{
"kind": "Update",
"path": "src/employee.rs",
"symbol": "struct Employee salary",
"description": "Change the type to an f32"
}
]
}
</incorrect_output>
<correct_output>
{
"title": "Change salary to f32",
"suggestions": [
{
"kind": "Update",
"path": "src/employee.rs",
"symbol": "struct Employee salary",
"description": "Change the type to an f32"
}
]
}
</correct_output>
<step_to_resolve>
Remove the 'department' field and update the 'print_details' method
</step_to_resolve>
<output>
{
"title": "Remove department",
"suggestions": [
{
"kind": "Delete",
"path": "src/employee.rs",
"symbol": "struct Employee department"
},
{
"kind": "Update",
"path": "src/employee.rs",
"symbol": "impl Employee fn print_details",
"description": "Don't print the 'department' field"
}
]
}
</output>
</example>
<example>
<workflow_context>
<message role="user">
```rs src/game.rs
struct Player {
name: String,
health: i32,
pub score: u32,
}
impl Player {
pub fn new(name: String) -> Self {
Player { name, health: 100, score: 0 }
}
}
struct Game {
players: Vec<Player>,
}
impl Game {
fn new() -> Self {
Game { players: Vec::new() }
}
}
```
</message>
<message role="assistant">
<step>Add a 'level' field to Player and update the 'new' method</step>
</message>
</workflow_context>
<step_to_resolve>
Add a 'level' field to Player and update the 'new' method
</step_to_resolve>
<output>
{
"title": "Add level field to Player",
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/game.rs",
"symbol": "struct Player pub score",
"description": "Add level field to Player"
},
{
"kind": "Update",
"path": "src/game.rs",
"symbol": "impl Player pub fn new",
"description": "Initialize level in new method"
}
]
}
</output>
</example>
<example>
<workflow_context>
<message role="user">
```rs src/config.rs
use std::collections::HashMap;
struct Config {
settings: HashMap<String, String>,
}
impl Config {
fn new() -> Self {
Config { settings: HashMap::new() }
}
}
```
</message>
<message role="assistant">
<step>Add a 'load_from_file' method to Config and import necessary modules</step>
</message>
</workflow_context>
<step_to_resolve>
Add a 'load_from_file' method to Config and import necessary modules
</step_to_resolve>
<output>
{
"title": "Add load_from_file method",
"suggestions": [
{
"kind": "PrependChild",
"path": "src/config.rs",
"symbol": "#imports",
"description": "Import std::fs and std::io modules"
},
{
"kind": "AppendChild",
"path": "src/config.rs",
"symbol": "impl Config",
"description": "Add load_from_file method"
}
]
}
</output>
</example>
<example>
<workflow_context>
<message role="user">
```rs src/database.rs
pub(crate) struct Database {
connection: Connection,
}
impl Database {
fn new(url: &str) -> Result<Self, Error> {
let connection = Connection::connect(url)?;
Ok(Database { connection })
}
async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
self.connection.query(sql, &[])
}
}
```
</message>
<message role="assistant">
<step>Add error handling to the 'query' method and create a custom error type</step>
</message>
</workflow_context>
<step_to_resolve>
Add error handling to the 'query' method and create a custom error type
</step_to_resolve>
<output>
{
"title": "Add error handling to query",
"suggestions": [
{
"kind": "PrependChild",
"path": "src/database.rs",
"description": "Import necessary error handling modules"
},
{
"kind": "InsertSiblingBefore",
"path": "src/database.rs",
"symbol": "pub(crate) struct Database",
"description": "Define custom DatabaseError enum"
},
{
"kind": "Update",
"path": "src/database.rs",
"symbol": "impl Database async fn query",
"description": "Implement error handling in query method"
}
]
}
</output>
</example>
</examples>
Now generate the suggestions for the following step:
<workflow_context>
{{{workflow_context}}}
</workflow_context>
<step_to_resolve>
{{{step_to_resolve}}}
</step_to_resolve>

View File

@@ -1,18 +0,0 @@
You are an expert terminal user.
You will be given a description of a command and you need to respond with a command that matches the description.
Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell.
Current OS name is '{{os}}', architecture is '{{arch}}'.
{{#if shell}}
Current shell is '{{shell}}'.
{{/if}}
{{#if working_directory}}
Current working directory is '{{working_directory}}'.
{{/if}}
{{#if latest_output}}
Latest non-empty terminal output:
{{#each latest_output as |line|}}
{{line}}
{{/each}}
{{/if}}
Here is the description of the command:
{{{user_prompt}}}

View File

@@ -26,9 +26,6 @@
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono",
// Set the buffer text's font fallbacks, this will be merged with
// the platform's default fallbacks.
"buffer_font_fallbacks": [],
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
@@ -50,11 +47,8 @@
// },
"buffer_line_height": "comfortable",
// The name of a font to use for rendering text in the UI
// You can set this to ".SystemUIFont" to use the system font
// (On macOS) You can set this to ".SystemUIFont" to use the system font
"ui_font_family": "Zed Plex Sans",
// Set the UI's font fallbacks, this will be merged with the platform's
// default font fallbacks.
"ui_font_fallbacks": [],
// The OpenType features to enable for text in the UI
"ui_font_features": {
// Disable ligatures:
@@ -64,15 +58,9 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
"pane_split_direction_vertical": "left",
// Centered layout related settings.
"centered_layout": {
// The relative width of the left padding of the central pane from the
@@ -324,7 +312,7 @@
"auto_reveal_entries": true,
// Whether to fold directories automatically and show compact folders
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
"auto_fold_dirs": true,
"auto_fold_dirs": false,
/// Scrollbar-related settings
"scrollbar": {
/// When to show the scrollbar in the project panel.
@@ -401,22 +389,9 @@
// The default model to use when creating new contexts.
"default_model": {
// The provider to use.
"provider": "zed.dev",
"provider": "openai",
// The model to use.
"model": "claude-3-5-sonnet"
}
},
// The settings for slash commands.
"slash_commands": {
// Settings for the `/docs` slash command.
"docs": {
// Whether `/docs` is enabled.
"enabled": false
},
// Settings for the `/project` slash command.
"project": {
// Whether `/project` is enabled.
"enabled": false
"model": "gpt-4o"
}
},
// Whether the screen sharing icon is shown in the os status bar.
@@ -654,7 +629,7 @@
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
@@ -700,10 +675,6 @@
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Plex Mono",
// Set the terminal's font fallbacks. If this option is not included,
// the terminal will default to matching the buffer's font fallbacks.
// This will be merged with the platform's default font fallbacks
// "font_fallbacks": ["FiraCode Nerd Fonts"],
// Sets the maximum number of lines in the terminal's scrollback buffer.
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
@@ -842,7 +813,6 @@
"language_servers": ["starpls", "!buck2-lsp", "..."]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
@@ -866,7 +836,6 @@
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"prettier": {
"allowed": true
}
@@ -886,18 +855,13 @@
// Different settings for specific language models.
"language_models": {
"anthropic": {
"version": "1",
"api_url": "https://api.anthropic.com"
},
"google": {
"api_url": "https://generativelanguage.googleapis.com"
"openai": {
"api_url": "https://api.openai.com/v1"
},
"ollama": {
"api_url": "http://localhost:11434"
},
"openai": {
"version": "1",
"api_url": "https://api.openai.com/v1"
}
},
// Zed's Prettier integration settings.
@@ -945,8 +909,7 @@
"vim": {
"use_system_clipboard": "always",
"use_multiline_find": false,
"use_smartcase_find": false,
"custom_digraphs": {}
"use_smartcase_find": false
},
// The server to connect to. If the environment variable
// ZED_SERVER_URL is set, it will override this setting.
@@ -1002,32 +965,5 @@
// {
// "W": "workspace::Save"
// }
"command_aliases": {},
// ssh_connections is an array of ssh connections.
// By default this setting is null, which disables the direct ssh connection support.
// You can configure these from `project: Open Remote` in the command palette.
// Zed's ssh support will pull configuration from your ~/.ssh too.
// Examples:
// [
// {
// "host": "example-box",
// "projects": [
// {
// "paths": ["/home/user/code/zed"]
// }
// ]
// }
// ]
"ssh_connections": null,
// Configures the Context Server Protocol binaries
//
// Examples:
// {
// "id": "server-1",
// "executable": "/path",
// "args": ['arg1", "args2"]
// }
"experimental.context_servers": {
"servers": []
}
"command_aliases": {}
}

View File

@@ -4,8 +4,8 @@
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette
// custom settings, run the `zed: Open Default Settings` command
// from the command palette
{
"ui_font_size": 16,
"buffer_font_size": 16,

View File

@@ -33,31 +33,5 @@ services:
volumes:
- ./livekit.yaml:/livekit.yaml
postgrest_app:
image: postgrest/postgrest
container_name: postgrest_app
ports:
- 8081:8081
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed
volumes:
- ./crates/collab/postgrest_app.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
postgrest_llm:
image: postgrest/postgrest
container_name: postgrest_llm
ports:
- 8082:8082
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed_llm
volumes:
- ./crates/collab/postgrest_llm.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
volumes:
postgres_data:

View File

@@ -3,9 +3,10 @@ use editor::Editor;
use extension::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
VisualContext as _,
};
use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
@@ -13,7 +14,7 @@ use language::{
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
use ui::{prelude::*, ContextMenu};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -26,7 +27,7 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
context_menu: Option<View<ContextMenu>>,
}
struct LspStatus {
@@ -78,7 +79,7 @@ impl ActivityIndicator {
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu_handle: Default::default(),
context_menu: None,
}
});
@@ -367,7 +368,72 @@ impl ActivityIndicator {
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
self.context_menu_handle.toggle(cx);
if self.context_menu.take().is_some() {
return;
}
self.build_lsp_work_context_menu(cx);
cx.notify();
}
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut has_work = false;
let this = cx.view().downgrade();
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
for work in self.pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
let title = SharedString::from(
work.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_string(),
);
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu.take();
})
.ok();
},
);
} else {
menu = menu.label(title.clone());
}
}
menu
});
if has_work {
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
})
.detach();
cx.focus_view(&context_menu);
self.context_menu = Some(context_menu);
cx.notify();
}
}
}
@@ -389,72 +455,19 @@ impl Render for ActivityIndicator {
on_click(this, cx);
}))
}
let this = cx.view().downgrade();
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
ButtonLike::new("activity-indicator-trigger").child(
h_flex()
.gap_2()
.children(content.icon)
.child(Label::new(content.message).size(LabelSize::Small)),
),
result
.gap_2()
.children(content.icon)
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
.children(self.context_menu.as_ref().map(|menu| {
deferred(
anchored()
.anchor(gpui::AnchorCorner::BottomLeft)
.child(menu.clone()),
)
.anchor(gpui::AnchorCorner::BottomLeft)
.menu(move |cx| {
let strong_this = this.upgrade()?;
ContextMenu::build(cx, |mut menu, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
let this = this.clone();
let mut title = work
.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_owned();
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
let title = SharedString::from(title);
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {
if let Some(progress_message) = work.progress.message.as_ref() {
title.push_str(": ");
title.push_str(progress_message);
}
menu = menu.label(title);
}
}
menu
})
.into()
}),
)
.with_priority(1)
}))
}
}

View File

@@ -17,7 +17,6 @@ path = "src/anthropic.rs"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
futures.workspace = true
http_client.workspace = true
isahc.workspace = true
@@ -25,8 +24,6 @@ schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -1,54 +1,27 @@
mod supported_countries;
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use isahc::http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use std::{pin::Pin, str::FromStr};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
pub use supported_countries::*;
use std::{convert::TryFrom, time::Duration};
use strum::EnumIter;
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct AnthropicModelCacheConfiguration {
pub min_total_token: usize,
pub should_speculate: bool,
pub max_cache_anchors: usize,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-20240620")]
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3-5-sonnet-20240620")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-20240229")]
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
Claude3Opus,
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-20240229")]
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
Claude3Sonnet,
#[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-20240307")]
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
Claude3Haiku,
#[serde(rename = "custom")]
Custom {
name: String,
max_tokens: usize,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
/// Override this model with a different Anthropic model for tool calls.
tool_override: Option<String>,
/// Indicates whether this custom model supports caching.
cache_configuration: Option<AnthropicModelCacheConfiguration>,
max_output_tokens: Option<u32>,
},
Custom { name: String, max_tokens: usize },
}
impl Model {
@@ -71,7 +44,7 @@ impl Model {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-20240620",
Model::Claude3Opus => "claude-3-opus-20240229",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Model::Claude3Haiku => "claude-3-opus-20240307",
Self::Custom { name, .. } => name,
}
}
@@ -82,24 +55,7 @@ impl Model {
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
}
}
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
}),
Self::Custom {
cache_configuration,
..
} => cache_configuration.clone(),
_ => None,
Self::Custom { name, .. } => name,
}
}
@@ -112,184 +68,138 @@ impl Model {
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
}
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
pub fn tool_model_id(&self) -> &str {
if let Self::Custom {
tool_override: Some(tool_override),
..
} = self
{
tool_override
} else {
self.id()
impl TryFrom<String> for Role {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self> {
match value.as_str() {
"user" => Ok(Self::User),
"assistant" => Ok(Self::Assistant),
_ => Err(anyhow!("invalid role '{value}'")),
}
}
}
pub async fn complete(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let response_message: Response =
serde_json::from_slice(&body).context("failed to deserialize response body")?;
Ok(response_message)
} else {
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str
)))
impl From<Role> for String {
fn from(val: Role) -> Self {
match val {
Role::User => "user".to_owned(),
Role::Assistant => "assistant".to_owned(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Request {
#[serde(serialize_with = "serialize_request_model")]
pub model: Model,
pub messages: Vec<RequestMessage>,
pub stream: bool,
pub system: String,
pub max_tokens: u32,
}
fn serialize_request_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&model.id())
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct RequestMessage {
pub role: Role,
pub content: String,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseEvent {
MessageStart {
message: ResponseMessage,
},
ContentBlockStart {
index: u32,
content_block: ContentBlock,
},
Ping {},
ContentBlockDelta {
index: u32,
delta: TextDelta,
},
ContentBlockStop {
index: u32,
},
MessageDelta {
delta: ResponseMessage,
usage: Usage,
},
MessageStop {},
}
#[derive(Deserialize, Debug)]
pub struct ResponseMessage {
#[serde(rename = "type")]
pub message_type: Option<String>,
pub id: Option<String>,
pub role: Option<String>,
pub content: Option<Vec<String>>,
pub model: Option<String>,
pub stop_reason: Option<String>,
pub stop_sequence: Option<String>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Debug)]
pub struct Usage {
pub input_tokens: Option<u32>,
pub output_tokens: Option<u32>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text { text: String },
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TextDelta {
TextDelta { text: String },
}
pub async fn stream_completion(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
.await
.map(|output| output.0)
}
/// https://docs.anthropic.com/en/api/rate-limits#response-headers
#[derive(Debug)]
pub struct RateLimitInfo {
pub requests_limit: usize,
pub requests_remaining: usize,
pub requests_reset: DateTime<Utc>,
pub tokens_limit: usize,
pub tokens_remaining: usize,
pub tokens_reset: DateTime<Utc>,
}
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
let tokens_remaining =
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
let requests_remaining =
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
Ok(Self {
requests_limit,
tokens_limit,
requests_remaining,
tokens_remaining,
requests_reset,
tokens_reset,
})
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
Ok(headers
.get(key)
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.to_str()?)
}
pub async fn stream_completion_with_rate_limit_info(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
Option<RateLimitInfo>,
),
AnthropicError,
> {
let request = StreamingRequest {
base: request,
stream: true,
};
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let uri = format!("{api_url}/v1/messages");
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("Anthropic-Beta", "tools-2024-04-04")
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
}
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
Ok(reader
.lines()
.filter_map(|line| async move {
match line {
@@ -297,329 +207,69 @@ pub async fn stream_completion_with_rate_limit_info(
let line = line.strip_prefix("data: ")?;
match serde_json::from_str(line) {
Ok(response) => Some(Ok(response)),
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
Err(error) => Some(Err(anyhow!(error))),
}
}
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
Err(error) => Some(Err(anyhow!(error))),
}
})
.boxed();
Ok((stream, rate_limits.log_err()))
.boxed())
} else {
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
response.body_mut().read_to_end(&mut body).await?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
let body_str = std::str::from_utf8(&body)?;
match serde_json::from_str::<Event>(body_str) {
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
Ok(_) => Err(AnthropicError::Other(anyhow!(
"Unexpected success response while expecting an error: '{body_str}'",
))),
Err(_) => Err(AnthropicError::Other(anyhow!(
match serde_json::from_str::<ResponseEvent>(body_str) {
Ok(_) => Err(anyhow!(
"Unexpected success response while expecting an error: {}",
body_str,
)),
Err(_) => Err(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str,
))),
)),
}
}
}
pub fn extract_text_from_events(
response: impl Stream<Item = Result<Event, AnthropicError>>,
) -> impl Stream<Item = Result<String, AnthropicError>> {
response.filter_map(|response| async move {
match response {
Ok(response) => match response {
Event::ContentBlockStart { content_block, .. } => match content_block {
Content::Text { text, .. } => Some(Ok(text)),
_ => None,
},
Event::ContentBlockDelta { delta, .. } => match delta {
ContentDelta::TextDelta { text } => Some(Ok(text)),
_ => None,
},
Event::Error { error } => Some(Err(AnthropicError::ApiError(error))),
_ => None,
},
Err(error) => Some(Err(error)),
}
})
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use http::IsahcHttpClient;
pub async fn extract_tool_args_from_events(
tool_name: String,
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
) -> Result<impl Send + Stream<Item = Result<String>>> {
let mut tool_use_index = None;
while let Some(event) = events.next().await {
if let Event::ContentBlockStart {
index,
content_block,
} = event?
{
if let Content::ToolUse { name, .. } = content_block {
if name == tool_name {
tool_use_index = Some(index);
break;
}
}
}
}
// #[tokio::test]
// async fn stream_completion_success() {
// let http_client = IsahcHttpClient::new().unwrap();
let Some(tool_use_index) = tool_use_index else {
return Err(anyhow!("tool not used"));
};
// let request = Request {
// model: Model::Claude3Opus,
// messages: vec![RequestMessage {
// role: Role::User,
// content: "Ping".to_string(),
// }],
// stream: true,
// system: "Respond to ping with pong".to_string(),
// max_tokens: 4096,
// };
Ok(events.filter_map(move |event| {
let result = match event {
Err(error) => Some(Err(error)),
Ok(Event::ContentBlockDelta { index, delta }) => match delta {
ContentDelta::TextDelta { .. } => None,
ContentDelta::InputJsonDelta { partial_json } => {
if index == tool_use_index {
Some(Ok(partial_json))
} else {
None
}
}
},
_ => None,
};
// let stream = stream_completion(
// &http_client,
// "https://api.anthropic.com",
// &std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY not set"),
// request,
// )
// .await
// .unwrap();
async move { result }
}))
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum CacheControlType {
Ephemeral,
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct CacheControl {
#[serde(rename = "type")]
pub cache_type: CacheControlType,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Vec<Content>,
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Content {
#[serde(rename = "text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "image")]
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
pub source_type: String,
pub media_type: String,
pub data: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
Any,
Tool { name: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<Message>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize)]
struct StreamingRequest {
#[serde(flatten)]
pub base: Request,
pub stream: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
pub user_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Usage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Response {
pub id: String,
#[serde(rename = "type")]
pub response_type: String,
pub role: Role,
pub content: Vec<Content>,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Event {
#[serde(rename = "message_start")]
MessageStart { message: Response },
#[serde(rename = "content_block_start")]
ContentBlockStart {
index: usize,
content_block: Content,
},
#[serde(rename = "content_block_delta")]
ContentBlockDelta { index: usize, delta: ContentDelta },
#[serde(rename = "content_block_stop")]
ContentBlockStop { index: usize },
#[serde(rename = "message_delta")]
MessageDelta { delta: MessageDelta, usage: Usage },
#[serde(rename = "message_stop")]
MessageStop,
#[serde(rename = "ping")]
Ping,
#[serde(rename = "error")]
Error { error: ApiError },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentDelta {
#[serde(rename = "text_delta")]
TextDelta { text: String },
#[serde(rename = "input_json_delta")]
InputJsonDelta { partial_json: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageDelta {
pub stop_reason: Option<String>,
pub stop_sequence: Option<String>,
}
#[derive(Error, Debug)]
pub enum AnthropicError {
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiError {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
/// An Anthropic API error code.
/// https://docs.anthropic.com/en/api/errors#http-errors
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum ApiErrorCode {
/// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
InvalidRequestError,
/// 401 - `authentication_error`: There's an issue with your API key.
AuthenticationError,
/// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
PermissionError,
/// 404 - `not_found_error`: The requested resource was not found.
NotFoundError,
/// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
RequestTooLarge,
/// 429 - `rate_limit_error`: Your account has hit a rate limit.
RateLimitError,
/// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
ApiError,
/// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
OverloadedError,
}
impl ApiError {
pub fn code(&self) -> Option<ApiErrorCode> {
ApiErrorCode::from_str(&self.error_type).ok()
}
pub fn is_rate_limit_error(&self) -> bool {
match self.error_type.as_str() {
"rate_limit_error" => true,
_ => false,
}
}
}
// stream
// .for_each(|event| async {
// match event {
// Ok(event) => println!("{:?}", event),
// Err(e) => eprintln!("Error: {:?}", e),
// }
// })
// .await;
// }
// }

View File

@@ -1,194 +0,0 @@
use std::collections::HashSet;
use std::sync::LazyLock;
/// Returns whether the given country code is supported by Anthropic.
///
/// https://www.anthropic.com/supported-countries
pub fn is_supported_country(country_code: &str) -> bool {
SUPPORTED_COUNTRIES.contains(&country_code)
}
/// The list of country codes supported by Anthropic.
///
/// https://www.anthropic.com/supported-countries
static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
vec![
"AL", // Albania
"DZ", // Algeria
"AD", // Andorra
"AO", // Angola
"AG", // Antigua and Barbuda
"AR", // Argentina
"AM", // Armenia
"AU", // Australia
"AT", // Austria
"AZ", // Azerbaijan
"BS", // Bahamas
"BH", // Bahrain
"BD", // Bangladesh
"BB", // Barbados
"BE", // Belgium
"BZ", // Belize
"BJ", // Benin
"BT", // Bhutan
"BO", // Bolivia
"BA", // Bosnia and Herzegovina
"BW", // Botswana
"BR", // Brazil
"BN", // Brunei
"BG", // Bulgaria
"BF", // Burkina Faso
"BI", // Burundi
"CV", // Cabo Verde
"KH", // Cambodia
"CM", // Cameroon
"CA", // Canada
"TD", // Chad
"CL", // Chile
"CO", // Colombia
"KM", // Comoros
"CG", // Congo (Brazzaville)
"CR", // Costa Rica
"CI", // Côte d'Ivoire
"HR", // Croatia
"CY", // Cyprus
"CZ", // Czechia (Czech Republic)
"DK", // Denmark
"DJ", // Djibouti
"DM", // Dominica
"DO", // Dominican Republic
"EC", // Ecuador
"EG", // Egypt
"SV", // El Salvador
"GQ", // Equatorial Guinea
"EE", // Estonia
"SZ", // Eswatini
"FJ", // Fiji
"FI", // Finland
"FR", // France
"GA", // Gabon
"GM", // Gambia
"GE", // Georgia
"DE", // Germany
"GH", // Ghana
"GR", // Greece
"GD", // Grenada
"GT", // Guatemala
"GN", // Guinea
"GW", // Guinea-Bissau
"GY", // Guyana
"HT", // Haiti
"HN", // Honduras
"HU", // Hungary
"IS", // Iceland
"IN", // India
"ID", // Indonesia
"IQ", // Iraq
"IE", // Ireland
"IL", // Israel
"IT", // Italy
"JM", // Jamaica
"JP", // Japan
"JO", // Jordan
"KZ", // Kazakhstan
"KE", // Kenya
"KI", // Kiribati
"KW", // Kuwait
"KG", // Kyrgyzstan
"LA", // Laos
"LV", // Latvia
"LB", // Lebanon
"LS", // Lesotho
"LR", // Liberia
"LI", // Liechtenstein
"LT", // Lithuania
"LU", // Luxembourg
"MG", // Madagascar
"MW", // Malawi
"MY", // Malaysia
"MV", // Maldives
"MT", // Malta
"MH", // Marshall Islands
"MR", // Mauritania
"MU", // Mauritius
"MX", // Mexico
"FM", // Micronesia
"MD", // Moldova
"MC", // Monaco
"MN", // Mongolia
"ME", // Montenegro
"MA", // Morocco
"MZ", // Mozambique
"NA", // Namibia
"NR", // Nauru
"NP", // Nepal
"NL", // Netherlands
"NZ", // New Zealand
"NE", // Niger
"NG", // Nigeria
"MK", // North Macedonia
"NO", // Norway
"OM", // Oman
"PK", // Pakistan
"PW", // Palau
"PS", // Palestine
"PA", // Panama
"PG", // Papua New Guinea
"PY", // Paraguay
"PE", // Peru
"PH", // Philippines
"PL", // Poland
"PT", // Portugal
"QA", // Qatar
"RO", // Romania
"RW", // Rwanda
"KN", // Saint Kitts and Nevis
"LC", // Saint Lucia
"VC", // Saint Vincent and the Grenadines
"WS", // Samoa
"SM", // San Marino
"ST", // São Tomé and Príncipe
"SA", // Saudi Arabia
"SN", // Senegal
"RS", // Serbia
"SC", // Seychelles
"SL", // Sierra Leone
"SG", // Singapore
"SK", // Slovakia
"SI", // Slovenia
"SB", // Solomon Islands
"ZA", // South Africa
"KR", // South Korea
"ES", // Spain
"LK", // Sri Lanka
"SR", // Suriname
"SE", // Sweden
"CH", // Switzerland
"TW", // Taiwan
"TJ", // Tajikistan
"TZ", // Tanzania
"TH", // Thailand
"TL", // Timor-Leste
"TG", // Togo
"TO", // Tonga
"TT", // Trinidad and Tobago
"TN", // Tunisia
"TR", // Türkiye (Turkey)
"TM", // Turkmenistan
"TV", // Tuvalu
"UG", // Uganda
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
"AE", // United Arab Emirates
"GB", // United Kingdom
"US", // United States of America
"UY", // Uruguay
"UZ", // Uzbekistan
"VU", // Vanuatu
"VA", // Vatican City
"VN", // Vietnam
"ZM", // Zambia
"ZW", // Zimbabwe
]
.into_iter()
.collect()
});

View File

@@ -11,7 +11,6 @@ use rust_embed::RustEmbed;
#[include = "themes/**/*"]
#[exclude = "themes/src/*"]
#[include = "sounds/**/*"]
#[include = "prompts/**/*"]
#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;

View File

@@ -32,16 +32,13 @@ client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
context_servers.workspace = true
completion.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
globset.workspace = true
gpui.workspace = true
handlebars.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
@@ -50,7 +47,6 @@ indoc.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
@@ -59,7 +55,6 @@ ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
proto.workspace = true
regex.workspace = true
rope.workspace = true
schemars.workspace = true
@@ -68,13 +63,11 @@ semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
similar.workspace = true
smol.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
toml.workspace = true
ui.workspace = true
@@ -82,17 +75,16 @@ util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
zed_actions.workspace = true
roxmltree = "0.20.0"
[dev-dependencies]
completion = { workspace = true, features = ["test-support"] }
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -38,7 +38,7 @@ Considering these aspects will ensure our conversation view design is optimized
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multiple people, or between multiple people and multiple bots (you and other bots).
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...

View File

@@ -1,5 +1,3 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
pub mod assistant_panel;
pub mod assistant_settings;
mod context;
@@ -9,23 +7,18 @@ mod model_selector;
mod prompt_library;
mod prompts;
mod slash_command;
pub(crate) mod slash_command_picker;
pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
mod workflow;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
use completion::LanguageModelCompletionProvider;
pub use context::*;
use context_servers::ContextServerRegistry;
pub use context_store::*;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::Context as _;
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
use indexed_docs::IndexedDocsRegistry;
pub(crate) use inline_assistant::*;
@@ -33,22 +26,16 @@ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub(crate) use model_selector::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::{
context_server_command, default_command, diagnostics_command, docs_command, fetch_command,
active_command, default_command, diagnostics_command, docs_command, fetch_command,
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
tab_command, terminal_command, workflow_command,
tabs_command, term_command,
};
use std::sync::Arc;
pub(crate) use streaming_diff::*;
use util::ResultExt;
pub use workflow::*;
use crate::slash_command_settings::SlashCommandSettings;
actions!(
assistant,
@@ -59,16 +46,16 @@ actions!(
QuoteSelection,
InsertIntoEditor,
ToggleFocus,
ResetKey,
InsertActivePrompt,
DeployHistory,
DeployPromptLibrary,
ConfirmCommand,
ToggleModelSelector,
DebugEditSteps
]
);
const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct InlineAssist {
prompt: Option<String>,
@@ -104,7 +91,6 @@ pub enum MessageStatus {
Pending,
Done,
Error(SharedString),
Canceled,
}
impl MessageStatus {
@@ -115,7 +101,6 @@ impl MessageStatus {
Some(proto::context_message_status::Variant::Error(error)) => {
MessageStatus::Error(error.message.into())
}
Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
None => MessageStatus::Pending,
}
}
@@ -139,11 +124,6 @@ impl MessageStatus {
},
)),
},
MessageStatus::Canceled => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Canceled(
proto::context_message_status::Canceled {},
)),
},
}
}
}
@@ -181,15 +161,9 @@ impl Assistant {
}
}
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
stdout_is_a_pty: bool,
cx: &mut AppContext,
) -> Arc<PromptBuilder> {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
// TODO: remove this when 0.148.0 is released.
if AssistantSettings::get_global(cx).using_outdated_settings_version {
@@ -218,34 +192,12 @@ pub fn init(
context_store::init(&client);
prompt_library::init(cx);
init_language_model_settings(cx);
init_completion_provider(cx);
assistant_slash_command::init(cx);
register_slash_commands(cx);
assistant_panel::init(cx);
context_servers::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
terminal_inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
IndexedDocsRegistry::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -263,86 +215,17 @@ pub fn init(
});
})
.detach();
register_context_server_handlers(cx);
prompt_builder
}
fn register_context_server_handlers(cx: &mut AppContext) {
cx.subscribe(
&context_servers::manager::ContextServerManager::global(cx),
|manager, event, cx| match event {
context_servers::manager::Event::ServerStarted { server_id } => {
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
}
}
})
.detach();
}
},
);
}
context_servers::manager::Event::ServerStopped { server_id } => {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(commands) = context_server_registry.get_commands(server_id) {
for command_name in commands {
slash_command_registry.unregister_command_by_name(&command_name);
context_server_registry.unregister_command(&server_id, &command_name);
}
}
}
},
)
.detach();
}
fn init_language_model_settings(cx: &mut AppContext) {
fn init_completion_provider(cx: &mut AppContext) {
completion::init(cx);
update_active_language_model_from_settings(cx);
cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
.detach();
cx.subscribe(
&LanguageModelRegistry::global(cx),
|_, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
update_active_language_model_from_settings(cx);
}
_ => {}
},
)
cx.observe(&LanguageModelRegistry::global(cx), |_, cx| {
update_active_language_model_from_settings(cx)
})
.detach();
}
@@ -350,61 +233,37 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
let settings = AssistantSettings::get_global(cx);
let provider_name = LanguageModelProviderId::from(settings.default_model.provider.clone());
let model_id = LanguageModelId::from(settings.default_model.model.clone());
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_active_model(&provider_name, &model_id, cx);
});
let Some(provider) = LanguageModelRegistry::global(cx)
.read(cx)
.provider(&provider_name)
else {
return;
};
let models = provider.provided_models(cx);
if let Some(model) = models.iter().find(|model| model.id() == model_id).cloned() {
LanguageModelCompletionProvider::global(cx).update(cx, |completion_provider, cx| {
completion_provider.set_active_model(model, cx);
});
}
}
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
fn register_slash_commands(cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, false);
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
slash_command_registry.register_command(term_command::TermSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder),
true,
);
}
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
update_slash_commands_from_settings(cx);
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach();
cx.observe_flag::<search_command::SearchSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
}
}
})
.detach();
}
fn update_slash_commands_from_settings(cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx);
if settings.docs.enabled {
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
} else {
slash_command_registry.unregister_command(docs_command::DocsSlashCommand);
}
if settings.project.enabled {
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
} else {
slash_command_registry.unregister_command(project_command::ProjectSlashCommand);
}
}
pub fn humanize_token_count(count: usize) -> String {

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@ pub struct AssistantSettings {
pub dock: AssistantDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: LanguageModelSelection,
pub default_model: AssistantDefaultModel,
pub using_outdated_settings_version: bool,
}
@@ -110,15 +110,11 @@ impl AssistantSettingsContent {
move |content, _| {
if content.anthropic.is_none() {
content.anthropic =
Some(language_model::settings::AnthropicSettingsContent::Versioned(
language_model::settings::VersionedAnthropicSettingsContent::V1(
language_model::settings::AnthropicSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models: None
}
)
));
Some(language_model::settings::AnthropicSettingsContent {
api_url,
low_speed_timeout_in_seconds,
..Default::default()
});
}
},
),
@@ -149,27 +145,12 @@ impl AssistantSettingsContent {
cx,
move |content, _| {
if content.openai.is_none() {
let available_models = available_models.map(|models| {
models
.into_iter()
.filter_map(|model| match model {
open_ai::Model::Custom { name, max_tokens,max_output_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens,max_output_tokens })
}
_ => None,
})
.collect::<Vec<_>>()
});
content.openai =
Some(language_model::settings::OpenAiSettingsContent::Versioned(
language_model::settings::VersionedOpenAiSettingsContent::V1(
language_model::settings::OpenAiSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models
}
)
));
Some(language_model::settings::OpenAiSettingsContent {
api_url,
low_speed_timeout_in_seconds,
available_models,
});
}
},
),
@@ -198,25 +179,25 @@ impl AssistantSettingsContent {
.clone()
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
default_model.map(|model| AssistantDefaultModel {
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
default_model.map(|model| AssistantDefaultModel {
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
default_model.map(|model| AssistantDefaultModel {
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
default_model.map(|model| AssistantDefaultModel {
provider: "ollama".to_string(),
model: model.id().to_string(),
})
@@ -231,7 +212,7 @@ impl AssistantSettingsContent {
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
default_model: Some(AssistantDefaultModel {
provider: "openai".to_string(),
model: settings
.default_open_ai_model
@@ -268,7 +249,9 @@ impl AssistantSettingsContent {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
"zed.dev" => {
log::warn!("attempted to set zed.dev model on outdated settings");
settings.provider = Some(AssistantProviderContentV1::ZedDotDev {
default_model: CloudModel::from_id(&model).ok(),
});
}
"anthropic" => {
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
@@ -325,7 +308,7 @@ impl AssistantSettingsContent {
_ => {}
},
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_model = Some(LanguageModelSelection { provider, model });
settings.default_model = Some(AssistantDefaultModel { provider, model });
}
},
AssistantSettingsContent::Legacy(settings) => {
@@ -382,11 +365,11 @@ pub struct AssistantSettingsContentV2 {
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new contexts.
default_model: Option<LanguageModelSelection>,
default_model: Option<AssistantDefaultModel>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
pub struct AssistantDefaultModel {
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
@@ -396,18 +379,16 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema:
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"google".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
impl Default for AssistantDefaultModel {
fn default() -> Self {
Self {
provider: "openai".to_string(),
@@ -440,7 +421,7 @@ pub struct AssistantSettingsContentV1 {
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "zed.dev"
/// This can either be the internal `zed.dev` service or an external `openai` service,
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}
@@ -476,8 +457,6 @@ pub struct LegacyAssistantSettingsContent {
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AssistantSettingsContent;
fn load(
@@ -519,70 +498,103 @@ fn merge<T>(target: &mut T, value: Option<T>) {
}
}
#[cfg(test)]
mod tests {
use gpui::{ReadGlobal, TestAppContext};
// #[cfg(test)]
// mod tests {
// use gpui::{AppContext, UpdateGlobal};
// use settings::SettingsStore;
use super::*;
// use super::*;
#[gpui::test]
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
// #[gpui::test]
// fn test_deserialize_assistant_settings(cx: &mut AppContext) {
// let store = settings::SettingsStore::test(cx);
// cx.set_global(store);
cx.update(|cx| {
let test_settings = settings::SettingsStore::test(cx);
cx.set_global(test_settings);
AssistantSettings::register(cx);
});
// // Settings default to gpt-4-turbo.
// AssistantSettings::register(cx);
// assert_eq!(
// AssistantSettings::get_global(cx).provider,
// AssistantProvider::OpenAi {
// model: OpenAiModel::FourOmni,
// api_url: open_ai::OPEN_AI_API_URL.into(),
// low_speed_timeout_in_seconds: None,
// available_models: Default::default(),
// }
// );
cx.update(|cx| {
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
}
);
});
// // Ensure backward-compatibility.
// SettingsStore::update_global(cx, |store, cx| {
// store
// .set_user_settings(
// r#"{
// "assistant": {
// "openai_api_url": "test-url",
// }
// }"#,
// cx,
// )
// .unwrap();
// });
// assert_eq!(
// AssistantSettings::get_global(cx).provider,
// AssistantProvider::OpenAi {
// model: OpenAiModel::FourOmni,
// api_url: "test-url".into(),
// low_speed_timeout_in_seconds: None,
// available_models: Default::default(),
// }
// );
// SettingsStore::update_global(cx, |store, cx| {
// store
// .set_user_settings(
// r#"{
// "assistant": {
// "default_open_ai_model": "gpt-4-0613"
// }
// }"#,
// cx,
// )
// .unwrap();
// });
// assert_eq!(
// AssistantSettings::get_global(cx).provider,
// AssistantProvider::OpenAi {
// model: OpenAiModel::Four,
// api_url: open_ai::OPEN_AI_API_URL.into(),
// low_speed_timeout_in_seconds: None,
// available_models: Default::default(),
// }
// );
cx.update(|cx| {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent::Versioned(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
}),
)
},
);
});
cx.run_until_parked();
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
assert!(raw_settings_value.contains(r#""version": "2""#));
#[derive(Debug, Deserialize)]
struct AssistantSettingsTest {
assistant: AssistantSettingsContent,
}
let assistant_settings: AssistantSettingsTest =
serde_json_lenient::from_str(&raw_settings_value).unwrap();
assert!(!assistant_settings.assistant.is_version_outdated());
}
}
// // The new version supports setting a custom model when using zed.dev.
// SettingsStore::update_global(cx, |store, cx| {
// store
// .set_user_settings(
// r#"{
// "assistant": {
// "version": "1",
// "provider": {
// "name": "zed.dev",
// "default_model": {
// "custom": {
// "name": "custom-provider"
// }
// }
// }
// }
// }"#,
// cx,
// )
// .unwrap();
// });
// assert_eq!(
// AssistantSettings::get_global(cx).provider,
// AssistantProvider::ZedDotDev {
// model: CloudModel::Custom {
// name: "custom-provider".into(),
// max_tokens: None
// }
// }
// );
// }
// }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use crate::{
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
SavedContext, SavedContextMetadata,
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
@@ -8,9 +8,7 @@ use clock::ReplicaId;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, Task, WeakModel};
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
@@ -28,7 +26,6 @@ use util::{ResultExt, TryFutureExt};
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
client.add_model_request_handler(ContextStore::handle_open_context);
client.add_model_request_handler(ContextStore::handle_create_context);
client.add_model_message_handler(ContextStore::handle_update_context);
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
}
@@ -52,15 +49,8 @@ pub struct ContextStore {
project_is_shared: bool,
client_subscription: Option<client::Subscription>,
_project_subscriptions: Vec<gpui::Subscription>,
prompt_builder: Arc<PromptBuilder>,
}
pub enum ContextStoreEvent {
ContextCreated(ContextId),
}
impl EventEmitter<ContextStoreEvent> for ContextStore {}
enum ContextHandle {
Weak(WeakModel<Context>),
Strong(Model<Context>),
@@ -83,11 +73,7 @@ impl ContextHandle {
}
impl ContextStore {
pub fn new(
project: Model<Project>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
let fs = project.read(cx).fs().clone();
let languages = project.read(cx).languages().clone();
let telemetry = project.read(cx).client().telemetry().clone();
@@ -122,7 +108,6 @@ impl ContextStore {
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project, cx);
this.synchronize_contexts(cx);
@@ -184,34 +169,6 @@ impl ContextStore {
})
}
async fn handle_create_context(
this: Model<Self>,
_: TypedEnvelope<proto::CreateContext>,
mut cx: AsyncAppContext,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_remote() {
return Err(anyhow!("can only create contexts as the host"));
}
let context = this.create(cx);
let context_id = context.read(cx).id().clone();
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
anyhow::Ok((
context_id,
context
.read(cx)
.serialize_ops(&ContextVersion::default(), cx),
))
})??;
let operations = operations.await;
Ok(proto::CreateContextResponse {
context_id: context_id.to_proto(),
context: Some(proto::Context { operations }),
})
}
async fn handle_update_context(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateContext>,
@@ -336,76 +293,12 @@ impl ContextStore {
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
Context::local(
self.languages.clone(),
Some(self.project.clone()),
Some(self.telemetry.clone()),
self.prompt_builder.clone(),
cx,
)
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
});
self.register_context(&context, cx);
context
}
pub fn create_remote_context(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
};
if project.is_local() {
return Task::ready(Err(anyhow!("cannot create remote contexts as the host")));
}
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let prompt_builder = self.prompt_builder.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_id = ContextId::from_proto(response.context_id);
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
Context::new(
context_id.clone(),
replica_id,
capability,
language_registry,
prompt_builder,
Some(project),
Some(telemetry),
cx,
)
})?;
let operations = cx
.background_executor()
.spawn(async move {
context_proto
.operations
.into_iter()
.map(|op| ContextOperation::from_proto(op))
.collect::<Result<Vec<_>>>()
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))??;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
}
})
})
}
pub fn open_local_context(
&mut self,
path: PathBuf,
@@ -417,7 +310,6 @@ impl ContextStore {
let fs = self.fs.clone();
let languages = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let load = cx.background_executor().spawn({
let path = path.clone();
@@ -426,20 +318,11 @@ impl ContextStore {
SavedContext::from_json(&saved_context)
}
});
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
Context::deserialize(
saved_context,
path.clone(),
languages,
prompt_builder,
Some(project),
Some(telemetry),
cx,
)
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
})?;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
@@ -463,11 +346,7 @@ impl ContextStore {
})
}
pub(super) fn loaded_context_for_id(
&self,
id: &ContextId,
cx: &AppContext,
) -> Option<Model<Context>> {
fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).id() == id {
@@ -498,13 +377,11 @@ impl ContextStore {
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::OpenContext {
project_id,
context_id: context_id.to_proto(),
});
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
@@ -514,8 +391,6 @@ impl ContextStore {
replica_id,
capability,
language_registry,
prompt_builder,
Some(project),
Some(telemetry),
cx,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,16 @@
use feature_flags::ZedPro;
use gpui::Action;
use gpui::DismissEvent;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
use workspace::ShowConfiguration;
use std::sync::Arc;
use ui::ListItemSpacing;
use crate::assistant_settings::AssistantSettings;
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
use fs::Fs;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use language_model::LanguageModelRegistry;
use settings::update_settings_file;
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
#[derive(IntoElement)]
pub struct ModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<ModelPickerDelegate>>>,
handle: Option<PopoverMenuHandle<ContextMenu>>,
fs: Arc<dyn Fs>,
trigger: T,
info_text: Option<SharedString>,
}
pub struct ModelPickerDelegate {
fs: Arc<dyn Fs>,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
}
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
impl<T: PopoverTrigger> ModelSelector<T> {
@@ -48,262 +19,110 @@ impl<T: PopoverTrigger> ModelSelector<T> {
handle: None,
fs,
trigger,
info_text: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>) -> Self {
pub fn with_handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
self.handle = Some(handle);
self
}
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
self.info_text = Some(text.into());
self
}
}
impl PickerDelegate for ModelPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_models.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_models.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a model...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_models = self.all_models.clone();
cx.spawn(|this, mut cx| async move {
let filtered_models = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_models
} else {
all_models
.into_iter()
.filter(|model_info| {
model_info
.model
.name()
.0
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.filtered_models = filtered_models;
this.delegate.set_selected_index(0, cx);
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(model_info) = self.filtered_models.get(self.selected_index) {
let model = model_info.model.clone();
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings, _| {
settings.set_model(model.clone())
});
// Update the selection status
let selected_model_id = model_info.model.id();
let selected_provider_id = model_info.model.provider_id();
for model in &mut self.all_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
for model in &mut self.filtered_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
use feature_flags::FeatureFlagAppExt;
let model_info = self.filtered_models.get(ix)?;
let show_badges = cx.has_flag::<ZedPro>();
let provider_name: String = model_info.model.provider_name().0.into();
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.start_slot(
div().pr_1().child(
Icon::new(model_info.icon)
.color(Color::Muted)
.size(IconSize::Medium),
),
)
.child(
h_flex()
.w_full()
.justify_between()
.font_buffer(cx)
.min_w(px(240.))
.child(
h_flex()
.gap_2()
.child(Label::new(model_info.model.name().0.clone()))
.child(
Label::new(provider_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
show_badges.then(|| {
Label::new("Pro")
.size(LabelSize::XSmall)
.color(Color::Muted)
})
}
}),
)
.child(div().when(model_info.is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
.size(IconSize::Small),
)
})),
),
)
}
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
use feature_flags::FeatureFlagAppExt;
let plan = proto::Plan::ZedPro;
let is_trial = false;
Some(
h_flex()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border)
.p_1()
.gap_4()
.justify_between()
.when(cx.has_flag::<ZedPro>(), |this| {
this.child(match plan {
// Already a zed pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(zed_actions::OpenAccountSettings))
}),
// Free user
Plan::Free => Button::new(
"try-pro",
if is_trial {
"Upgrade to Pro"
} else {
"Try Pro"
},
)
.on_click(|_, cx| cx.open_url(TRY_ZED_PRO_URL)),
})
})
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, cx| {
cx.dispatch_action(ShowConfiguration.boxed_clone());
}),
)
.into_any(),
)
}
}
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let selected_model = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
fn render(self, _: &mut WindowContext) -> impl IntoElement {
let mut menu = PopoverMenu::new("model-switcher");
if let Some(handle) = self.handle {
menu = menu.with_handle(handle);
}
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),
menu.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for (index, provider) in LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.enumerate()
{
if index > 0 {
menu = menu.separator();
}
})
menu = menu.header(provider.name().0);
let available_models = provider.provided_models(cx);
if available_models.is_empty() {
menu = menu.custom_entry(
{
move |_| {
h_flex()
.w_full()
.gap_1()
.child(Icon::new(IconName::Settings))
.child(Label::new("Configure"))
.into_any()
}
},
{
let provider = provider.id();
move |cx| {
LanguageModelCompletionProvider::global(cx).update(
cx,
|completion_provider, cx| {
completion_provider
.set_active_provider(provider.clone(), cx)
},
);
}
},
);
}
let selected_model = LanguageModelCompletionProvider::read_global(cx)
.active_model()
.map(|m| m.id());
let selected_provider = LanguageModelCompletionProvider::read_global(cx)
.active_provider()
.map(|m| m.id());
for available_model in available_models {
menu = menu.custom_entry(
{
let id = available_model.id();
let provider_id = available_model.provider_id();
let model_name = available_model.name().0.clone();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(model_name.clone()))
.when(
selected_model.as_ref() == Some(&id)
&& selected_provider.as_ref() == Some(&provider_id),
|this| this.child(Icon::new(IconName::Check)),
)
.into_any()
}
},
{
let fs = self.fs.clone();
let model = available_model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model),
);
}
},
);
}
}
menu
})
.collect::<Vec<_>>();
let delegate = ModelPickerDelegate {
fs: self.fs.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
.into()
})
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
}
}

View File

@@ -1,7 +1,9 @@
use crate::{
slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
LanguageModelCompletionProvider,
};
use anyhow::{anyhow, Result};
use assets::Assets;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
@@ -11,18 +13,13 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
types::{SerdeBincode, SerdeJson, Str},
Database, RoTxn,
};
use heed::{types::SerdeBincode, Database, RoTxn};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
use parking_lot::RwLock;
use picker::{Picker, PickerDelegate};
use rope::Rope;
@@ -35,10 +32,9 @@ use std::{
sync::{atomic::AtomicBool, Arc},
time::Duration,
};
use text::LineEnding;
use theme::ThemeSettings;
use ui::{
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, Tooltip, ViewContext, VisualContext,
};
use util::{ResultExt, TryFutureExt};
@@ -66,11 +62,6 @@ pub fn init(cx: &mut AppContext) {
cx.set_global(GlobalPromptStore(prompt_store_future))
}
const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!(
"This prompt supports special functionality.\n",
"It's read-only, but you can remove it from your default prompt."
);
/// This function opens a new prompt library window if one doesn't exist already.
/// If one exists, it brings it to the foreground.
///
@@ -100,7 +91,7 @@ pub fn open_prompt_library(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("Prompt Library".into()),
appears_transparent: !cfg!(windows),
appears_transparent: true,
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -155,14 +146,6 @@ impl PickerDelegate for PromptPickerDelegate {
self.matches.len()
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
if self.store.prompt_count() == 0 {
"No prompts.".into()
} else {
"No prompts found matching your search.".into()
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
@@ -246,29 +229,15 @@ impl PickerDelegate for PromptPickerDelegate {
.end_hover_slot(
h_flex()
.gap_2()
.child(if prompt_id.is_built_in() {
div()
.id("built-in-prompt")
.child(Icon::new(IconName::FileLock).color(Color::Muted))
.tooltip(move |cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
cx,
)
})
.into_any()
} else {
.child(
IconButton::new("delete-prompt", IconName::Trash)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
}))
.into_any_element()
})
})),
)
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.selected(default)
@@ -381,10 +350,6 @@ impl PromptLibrary {
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
if prompt_id.is_built_in() {
return;
}
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
let title = prompt_editor.title_editor.read(cx).text(cx);
@@ -494,10 +459,6 @@ impl PromptLibrary {
let mut editor = Editor::auto_width(cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(false);
}
editor
});
let body_editor = cx.new_view(|cx| {
@@ -509,10 +470,6 @@ impl PromptLibrary {
});
let mut editor = Editor::for_buffer(buffer, None, cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(false);
}
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
@@ -679,10 +636,7 @@ impl PromptLibrary {
};
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
let provider = LanguageModelCompletionProvider::read_global(cx);
let initial_prompt = action.prompt.clone();
if provider.is_authenticated(cx) {
InlineAssistant::update_global(cx, |assistant, cx| {
@@ -771,9 +725,6 @@ impl PromptLibrary {
}
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
let editor = &prompt.body_editor.read(cx);
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
@@ -785,12 +736,11 @@ impl PromptLibrary {
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let token_count = cx
.update(|cx| {
model.count_tokens(
LanguageModelCompletionProvider::read_global(cx).count_tokens(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::System,
content: vec![body.to_string().into()],
cache: false,
content: body.to_string(),
}],
stop: Vec::new(),
temperature: 1.,
@@ -799,7 +749,6 @@ impl PromptLibrary {
)
})?
.await?;
this.update(&mut cx, |this, cx| {
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
prompt_editor.token_count = Some(token_count);
@@ -854,7 +803,7 @@ impl PromptLibrary {
let prompt_metadata = self.store.metadata(prompt_id)?;
let prompt_editor = &self.prompt_editors[&prompt_id];
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
let model = LanguageModelRegistry::read_global(cx).active_model();
let current_model = LanguageModelCompletionProvider::read_global(cx).active_model();
let settings = ThemeSettings::get_global(cx);
Some(
@@ -926,7 +875,6 @@ impl PromptLibrary {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
)),
),
@@ -965,7 +913,7 @@ impl PromptLibrary {
None,
format!(
"Model: {}",
model
current_model
.as_ref()
.map(|model| model
.name()
@@ -984,23 +932,7 @@ impl PromptLibrary {
)
},
))
.child(if prompt_id.is_built_in() {
div()
.id("built-in-prompt")
.child(
Icon::new(IconName::FileLock)
.color(Color::Muted),
)
.tooltip(move |cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
cx,
)
})
.into_any()
} else {
.child(
IconButton::new(
"delete-prompt",
IconName::Trash,
@@ -1018,9 +950,8 @@ impl PromptLibrary {
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(DeletePrompt));
})
.into_any_element()
})
}),
)
.child(
IconButton::new(
"duplicate-prompt",
@@ -1110,55 +1041,7 @@ impl Render for PromptLibrary {
.font(ui_font)
.text_color(theme.colors().text)
.child(self.render_prompt_list(cx))
.map(|el| {
if self.store.prompt_count() == 0 {
el.child(
v_flex()
.w_2_3()
.h_full()
.items_center()
.justify_center()
.gap_4()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.gap_2()
.child(
Icon::new(IconName::Book)
.size(IconSize::Medium)
.color(Color::Muted),
)
.child(
Label::new("No prompts yet")
.size(LabelSize::Large)
.color(Color::Muted),
),
)
.child(
h_flex()
.child(h_flex())
.child(
v_flex()
.gap_1()
.child(Label::new("Create your first prompt:"))
.child(
Button::new("create-prompt", "New Prompt")
.full_width()
.key_binding(KeyBinding::for_action(
&NewPrompt, cx,
))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone())
}),
),
)
.child(h_flex()),
),
)
} else {
el.child(self.render_active_prompt(cx))
}
})
.child(self.render_active_prompt(cx))
}
}
@@ -1171,30 +1054,20 @@ pub struct PromptMetadata {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum PromptId {
User { uuid: Uuid },
EditWorkflow,
}
pub struct PromptId(Uuid);
impl PromptId {
pub fn new() -> PromptId {
PromptId::User {
uuid: Uuid::new_v4(),
}
}
pub fn is_built_in(&self) -> bool {
!matches!(self, PromptId::User { .. })
PromptId(Uuid::new_v4())
}
}
pub struct PromptStore {
executor: BackgroundExecutor,
env: heed::Env,
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
metadata_cache: RwLock<MetadataCache>,
metadata: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
bodies: Database<SerdeJson<PromptId>, Str>,
}
#[derive(Default)]
@@ -1205,7 +1078,7 @@ struct MetadataCache {
impl MetadataCache {
fn from_db(
db: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
db: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
txn: &RoTxn,
) -> Result<Self> {
let mut cache = MetadataCache::default();
@@ -1257,123 +1130,35 @@ impl PromptStore {
let db_env = unsafe {
heed::EnvOpenOptions::new()
.map_size(1024 * 1024 * 1024) // 1GB
.max_dbs(4) // Metadata and bodies (possibly v1 of both as well)
.max_dbs(2) // bodies and metadata
.open(db_path)?
};
let mut txn = db_env.write_txn()?;
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
// Remove edit workflow prompt, as we decided to opt into it using
// a slash command instead.
metadata.delete(&mut txn, &PromptId::EditWorkflow).ok();
bodies.delete(&mut txn, &PromptId::EditWorkflow).ok();
txn.commit()?;
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
let txn = db_env.read_txn()?;
let bodies = db_env.create_database(&mut txn, Some("bodies"))?;
let metadata = db_env.create_database(&mut txn, Some("metadata"))?;
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
txn.commit()?;
Ok(PromptStore {
executor,
env: db_env,
metadata_cache: RwLock::new(metadata_cache),
metadata,
bodies,
metadata,
metadata_cache: RwLock::new(metadata_cache),
})
}
})
}
fn upgrade_dbs(
env: &heed::Env,
metadata_db: heed::Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
bodies_db: heed::Database<SerdeJson<PromptId>, Str>,
) -> Result<()> {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub struct PromptIdV1(Uuid);
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PromptMetadataV1 {
pub id: PromptIdV1,
pub title: Option<SharedString>,
pub default: bool,
pub saved_at: DateTime<Utc>,
}
let mut txn = env.write_txn()?;
let Some(bodies_v1_db) = env
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<String>>(
&txn,
Some("bodies"),
)?
else {
return Ok(());
};
let mut bodies_v1 = bodies_v1_db
.iter(&txn)?
.collect::<heed::Result<HashMap<_, _>>>()?;
let Some(metadata_v1_db) = env
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<PromptMetadataV1>>(
&txn,
Some("metadata"),
)?
else {
return Ok(());
};
let metadata_v1 = metadata_v1_db
.iter(&txn)?
.collect::<heed::Result<HashMap<_, _>>>()?;
for (prompt_id_v1, metadata_v1) in metadata_v1 {
let prompt_id_v2 = PromptId::User {
uuid: prompt_id_v1.0,
};
let Some(body_v1) = bodies_v1.remove(&prompt_id_v1) else {
continue;
};
if metadata_db
.get(&txn, &prompt_id_v2)?
.map_or(true, |metadata_v2| {
metadata_v1.saved_at > metadata_v2.saved_at
})
{
metadata_db.put(
&mut txn,
&prompt_id_v2,
&PromptMetadata {
id: prompt_id_v2,
title: metadata_v1.title.clone(),
default: metadata_v1.default,
saved_at: metadata_v1.saved_at,
},
)?;
bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?;
}
}
txn.commit()?;
Ok(())
}
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
let env = self.env.clone();
let bodies = self.bodies;
self.executor.spawn(async move {
let txn = env.read_txn()?;
let mut prompt = bodies
bodies
.get(&txn, &id)?
.ok_or_else(|| anyhow!("prompt not found"))?
.into();
LineEnding::normalize(&mut prompt);
Ok(prompt)
.ok_or_else(|| anyhow!("prompt not found"))
})
}
@@ -1406,11 +1191,6 @@ impl PromptStore {
})
}
/// Returns the number of prompts in the store.
fn prompt_count(&self) -> usize {
self.metadata_cache.read().metadata.len()
}
fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
}
@@ -1467,10 +1247,6 @@ impl PromptStore {
default: bool,
body: Rope,
) -> Task<Result<()>> {
if id.is_built_in() {
return Task::ready(Err(anyhow!("built-in prompts cannot be saved")));
}
let prompt_metadata = PromptMetadata {
id,
title,
@@ -1498,26 +1274,16 @@ impl PromptStore {
fn save_metadata(
&self,
id: PromptId,
mut title: Option<SharedString>,
title: Option<SharedString>,
default: bool,
) -> Task<Result<()>> {
let mut cache = self.metadata_cache.write();
if id.is_built_in() {
title = cache
.metadata_by_id
.get(&id)
.and_then(|metadata| metadata.title.clone());
}
let prompt_metadata = PromptMetadata {
id,
title,
default,
saved_at: Utc::now(),
};
cache.insert(prompt_metadata.clone());
self.metadata_cache.write().insert(prompt_metadata.clone());
let db_connection = self.env.clone();
let metadata = self.metadata;
@@ -1534,6 +1300,17 @@ impl PromptStore {
fn first(&self) -> Option<PromptMetadata> {
self.metadata_cache.read().metadata.first().cloned()
}
pub fn operations_prompt(&self) -> String {
String::from_utf8(
Assets
.load("prompts/operations.md")
.unwrap()
.unwrap()
.to_vec(),
)
.unwrap()
}
}
/// Wraps a shared future to a prompt store so it can be assigned as a context global.

View File

@@ -1,306 +1,135 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use language::BufferSnapshot;
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use util::ResultExt;
use std::{fmt::Write, ops::Range};
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
pub language_name: Option<String>,
pub is_insert: bool,
pub is_truncated: bool,
pub document_content: String,
pub user_prompt: String,
pub rewrite_section: Option<String>,
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
_project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
#[derive(Serialize)]
pub struct TerminalAssistantPromptContext {
pub os: String,
pub arch: String,
pub shell: Option<String>,
pub working_directory: Option<String>,
pub latest_output: Vec<String>,
pub user_prompt: String,
}
/// Context required to generate a workflow step resolution prompt.
#[derive(Debug, Serialize)]
pub struct StepResolutionContext {
/// The full context, including <step>...</step> tags
pub workflow_context: String,
/// The text of the specific step from the context to resolve
pub step_to_resolve: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
impl PromptBuilder {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(
prompt,
"Here's a file of text that I'm going to ask you to make an edit to."
)?;
"text"
}
Some(language_name) => {
writeln!(
prompt,
"Here's a file of {language_name} that I'm going to ask you to make an edit to."
)?;
"code"
}
};
Ok(Self { handlebars })
const MAX_CTX: usize = 50000;
let mut is_truncated = false;
if range.is_empty() {
prompt.push_str("The point you'll need to insert at is marked with <insert_here></insert_here>.\n\n<document>");
} else {
prompt.push_str("The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.\n\n<document>");
}
// Include file content.
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
range.start - MAX_CTX..range.start
} else {
before_range
};
let mut non_rewrite_len = truncated_before.len();
for chunk in buffer.text_for_range(truncated_before) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
prompt.push_str("\n<rewrite_this>");
} else {
prompt.push_str("<insert_here></insert_here>");
}
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
range.end..range.end + MAX_CTX
} else {
after_range
};
non_rewrite_len += truncated_after.len();
for chunk in buffer.text_for_range(truncated_after) {
prompt.push_str(chunk);
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
mut params: PromptLoadingParams,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
params.repo_path = None;
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
.spawn(async move {
let Some(parent_dir) = templates_dir.parent() else {
return;
};
write!(prompt, "</document>\n\n").unwrap();
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
}
found_dir_once = true;
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::info!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for changed_path in changed_paths {
if changed_path.starts_with(&templates_dir) && changed_path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", changed_path.display());
if let Some(content) = params.fs.load(&changed_path).await.log_err() {
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
})
.detach();
if is_truncated {
writeln!(prompt, "The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.\n")?;
}
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::info!("Registering built-in prompt template: {}", id);
handlebars
.register_template_string(id, String::from_utf8_lossy(prompt.as_ref()))?
}
}
}
Ok(())
}
pub fn generate_content_prompt(
&self,
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => "text",
Some(_) => "code",
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
range.start - MAX_CTX..range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
range.end..range.end + MAX_CTX
} else {
after_range
};
let mut document_content = String::new();
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
if range.is_empty() {
writeln!(
prompt,
"You can't replace {content_type}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the following prompt:\n\n<prompt>\n{user_prompt}\n</prompt>",
)
.unwrap();
writeln!(prompt, "Match the indentation in the original file in the inserted {content_type}, don't include any indentation on blank lines.\n").unwrap();
prompt.push_str("Immediately start with the following format with no remarks:\n\n```\n{{INSERTED_CODE}}\n```");
} else {
writeln!(prompt, "Edit the section of {content_type} in <rewrite_this></rewrite_this> tags based on the following prompt:'").unwrap();
writeln!(prompt, "\n<prompt>\n{user_prompt}\n</prompt>\n").unwrap();
let rewrite_len = range.end - range.start;
if rewrite_len < 20000 && rewrite_len * 2 < non_rewrite_len {
writeln!(prompt, "And here's the section to rewrite based on that prompt again for reference:\n\n<rewrite_this>\n").unwrap();
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
prompt.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
writeln!(prompt, "\n</rewrite_this>\n").unwrap();
}
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
}
Some(section)
} else {
None
};
let context = ContentPromptContext {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_insert,
is_truncated,
document_content,
user_prompt,
rewrite_section,
};
self.handlebars.lock().render("content_prompt", &context)
writeln!(prompt, "Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {content_type} will be preserved.\n").unwrap();
write!(
prompt,
"Start at the indentation level in the original file in the rewritten {content_type}. "
)
.unwrap();
prompt.push_str("Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.");
prompt.push_str("\n\nImmediately start with the following format with no remarks:\n\n```\n{{REWRITTEN_CODE}}\n```");
}
pub fn generate_terminal_assistant_prompt(
&self,
user_prompt: &str,
shell: Option<&str>,
working_directory: Option<&str>,
latest_output: &[String],
) -> Result<String, RenderError> {
let context = TerminalAssistantPromptContext {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
shell: shell.map(|s| s.to_string()),
working_directory: working_directory.map(|s| s.to_string()),
latest_output: latest_output.to_vec(),
user_prompt: user_prompt.to_string(),
};
self.handlebars
.lock()
.render("terminal_assistant_prompt", &context)
}
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("edit_workflow", &())
}
pub fn generate_step_resolution_prompt(
&self,
context: &StepResolutionContext,
) -> Result<String, RenderError> {
self.handlebars.lock().render("step_resolution", context)
}
Ok(prompt)
}
pub fn generate_terminal_assistant_prompt(
user_prompt: &str,
shell: Option<&str>,
working_directory: Option<&str>,
) -> String {
let mut prompt = String::new();
writeln!(&mut prompt, "You are an expert terminal user.").unwrap();
writeln!(&mut prompt, "You will be given a description of a command and you need to respond with a command that matches the description.").unwrap();
writeln!(&mut prompt, "Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell.").unwrap();
if let Some(shell) = shell {
writeln!(&mut prompt, "Current shell is '{shell}'.").unwrap();
}
if let Some(working_directory) = working_directory {
writeln!(
&mut prompt,
"Current working directory is '{working_directory}'."
)
.unwrap();
}
writeln!(&mut prompt, "Here is the description of the command:").unwrap();
prompt.push_str(user_prompt);
prompt
}

View File

@@ -1,13 +1,11 @@
use crate::assistant_panel::ContextEditor;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::CompletionIntent;
use rope::Point;
use std::{
ops::Range,
@@ -19,7 +17,7 @@ use std::{
use ui::ActiveTheme;
use workspace::Workspace;
pub mod context_server_command;
pub mod active_command;
pub mod default_command;
pub mod diagnostics_command;
pub mod docs_command;
@@ -30,9 +28,8 @@ pub mod project_command;
pub mod prompt_command;
pub mod search_command;
pub mod symbols_command;
pub mod tab_command;
pub mod terminal_command;
pub mod workflow_command;
pub mod tabs_command;
pub mod term_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,
@@ -43,8 +40,8 @@ pub(crate) struct SlashCommandCompletionProvider {
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// Ranges within the line containing the command arguments.
pub arguments: Vec<Range<usize>>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
}
impl SlashCommandCompletionProvider {
@@ -98,45 +95,34 @@ impl SlashCommandCompletionProvider {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
if requires_argument {
new_text.push(' ');
}
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let confirm = editor.clone().zip(workspace.clone()).and_then(
|(editor, workspace)| {
(!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
false,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
});
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
})
},
);
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
@@ -144,6 +130,7 @@ impl SlashCommandCompletionProvider {
label: command.label(cx),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: requires_argument,
confirm,
})
})
@@ -155,20 +142,20 @@ impl SlashCommandCompletionProvider {
fn complete_command_argument(
&self,
command_name: &str,
arguments: &[String],
argument: String,
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
let completions = command.complete_argument(
arguments,
argument,
new_cancel_flag.clone(),
self.workspace.clone(),
cx,
@@ -176,76 +163,61 @@ impl SlashCommandCompletionProvider {
let command_name: Arc<str> = command_name.into();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
.map(|command_argument| {
let confirm = if command_argument.run_command {
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent, cx: &mut WindowContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&completed_arguments,
true,
false,
workspace.clone(),
cx,
);
})
.ok();
false
} else {
!new_argument.after_completion.run()
}
let command_argument = command_argument.new_text.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
}
}) as Arc<_>
});
})
} else {
None
};
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
let mut new_text = command_argument.new_text.clone();
if !command_argument.run_command {
new_text.push(' ');
}
project::Completion {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.label, None),
new_text,
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: !command_argument.run_command,
confirm,
}
})
.collect())
})
} else {
Task::ready(Ok(Vec::new()))
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
}
}
}
@@ -258,7 +230,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, arguments, command_range, last_argument_range)) =
let Some((name, argument, command_range, argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
@@ -269,52 +241,32 @@ impl CompletionProvider for SlashCommandCompletionProvider {
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
let command_range_end = Point::new(
position.row,
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
);
let command_range = buffer.anchor_after(command_range_start)
..buffer.anchor_after(command_range_end);
let name = line[call.name.clone()].to_string();
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
{
let last_arg_start =
Some(if let Some(argument) = call.argument {
let start =
buffer.anchor_after(Point::new(position.row, argument.start as u32));
let first_arg_start = call.arguments.first().expect("we have the last element");
let first_arg_start =
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.iter()
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
.collect::<Vec<_>>();
let argument_range = first_arg_start..buffer_position;
(
Some((arguments, argument_range)),
last_arg_start..buffer_position,
)
let argument = line[argument.clone()].to_string();
(name, Some(argument), command_range, start..buffer_position)
} else {
let start =
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
(None, start..buffer_position)
};
Some((name, arguments, command_range, last_argument_range))
(name, None, command_range, start..buffer_position)
})
})
else {
return Task::ready(Ok(Vec::new()));
};
if let Some((arguments, argument_range)) = arguments {
self.complete_command_argument(
&name,
&arguments,
command_range,
argument_range,
last_argument_range,
cx,
)
if let Some(argument) = argument {
self.complete_command_argument(&name, argument, command_range, argument_range, cx)
} else {
self.complete_command_name(&name, command_range, last_argument_range, cx)
self.complete_command_name(&name, command_range, argument_range, cx)
}
}
@@ -356,10 +308,6 @@ impl CompletionProvider for SlashCommandCompletionProvider {
false
}
}
fn sort_completions(&self) -> bool {
false
}
}
impl SlashCommandLine {
@@ -371,23 +319,16 @@ impl SlashCommandLine {
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = call.arguments.last_mut() {
if c.is_whitespace() {
if (*argument).is_empty() {
argument.start = next_ix;
argument.end = next_ix;
} else {
argument.end = ix;
call.arguments.push(next_ix..next_ix);
}
} else {
argument.end = next_ix;
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.arguments = vec![next_ix..next_ix];
call.argument = Some(next_ix..next_ix);
} else {
call.name.end = next_ix;
}
@@ -403,7 +344,7 @@ impl SlashCommandLine {
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
arguments: Vec::new(),
argument: None,
});
}
// The line can't contain anything before the slash except for whitespace.

View File

@@ -0,0 +1,102 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct ActiveSlashCommand;
impl SlashCommand for ActiveSlashCommand {
fn name(&self) -> String {
"active".into()
}
fn description(&self) -> String {
"insert active tab".into()
}
fn menu_text(&self) -> String {
"Insert Active Tab".into()
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};
let Some(buffer) = active_item
.downcast::<Editor>()
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
else {
return Task::ready(Err(anyhow!("active tab is not an editor")));
};
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
let task = cx.background_executor().spawn({
let path = path.clone();
async move {
let mut output = String::new();
output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```\n");
let has_diagnostics =
write_single_file_diagnostics(&mut output, path.as_deref(), &snapshot);
if output.ends_with('\n') {
output.pop();
}
(output, has_diagnostics)
}
});
cx.foreground_executor().spawn(async move {
let (text, has_diagnostics) = task.await;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![build_entry_output_section(
range,
path.as_deref(),
false,
None,
)],
run_commands_in_text: has_diagnostics,
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,134 +0,0 @@
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use collections::HashMap;
use context_servers::{
manager::{ContextServer, ContextServerManager},
protocol::PromptInfo,
};
use gpui::{Task, WeakView, WindowContext};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString};
use workspace::Workspace;
pub struct ContextServerSlashCommand {
server_id: String,
prompt: PromptInfo,
}
impl ContextServerSlashCommand {
pub fn new(server: &Arc<ContextServer>, prompt: PromptInfo) -> Self {
Self {
server_id: server.id.clone(),
prompt,
}
}
}
impl SlashCommand for ContextServerSlashCommand {
fn name(&self) -> String {
self.prompt.name.clone()
}
fn description(&self) -> String {
format!("Run context server command: {}", self.prompt.name)
}
fn menu_text(&self) -> String {
format!("Run '{}' from {}", self.prompt.name, self.server_id)
}
fn requires_argument(&self) -> bool {
self.prompt
.arguments
.as_ref()
.map_or(false, |args| !args.is_empty())
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
let prompt_args = match prompt_arguments(&self.prompt, arguments) {
Ok(args) => args,
Err(e) => return Task::ready(Err(e)),
};
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
if let Some(server) = manager.get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client.read().clone() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..result.len(),
icon: IconName::ZedAssistant,
label: SharedString::from(format!("Result from {}", prompt_name)),
}],
text: result,
run_commands_in_text: false,
})
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}
}
}
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
map.insert(args[0].name.clone(), arguments.join(" "));
Ok(map)
} else {
Err(anyhow!("Prompt expects argument but none given"))
}
}
Some(_) | None => {
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
}
}
}
/// MCP servers can return prompts with multiple arguments. Since we only
/// support one argument, we ignore all others. This is the necessary predicate
/// for this.
pub fn acceptable_prompt(prompt: &PromptInfo) -> bool {
match &prompt.arguments {
None => true,
Some(args) if args.len() == 1 => true,
_ => false,
}
}

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{
fmt::Write,
@@ -32,19 +32,19 @@ impl SlashCommand for DefaultSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let store = PromptStore::global(cx);

View File

@@ -33,7 +33,7 @@ impl DiagnosticsSlashCommand {
if query.is_empty() {
let workspace = workspace.read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let path_prefix: Arc<str> = Arc::default();
let path_prefix: Arc<str> = "".into();
Task::ready(
entries
.into_iter()
@@ -43,7 +43,6 @@ impl DiagnosticsSlashCommand {
worktree_id: entry.worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
is_dir: false, // Diagnostics can't be produced for directories
distance_to_relative_ancestor: 0,
})
.collect(),
@@ -103,21 +102,17 @@ impl SlashCommand for DiagnosticsSlashCommand {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let query = arguments.last().cloned().unwrap_or_default();
let query = query.split_whitespace().last().unwrap_or("").to_string();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
let executor = cx.background_executor().clone();
@@ -151,10 +146,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
Ok(matches
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.clone().into(),
label: completion.clone(),
new_text: completion,
after_completion: assistant_slash_command::AfterCompletion::Run,
replace_previous_arguments: false,
run_command: true,
})
.collect())
})
@@ -162,78 +156,70 @@ impl SlashCommand for DiagnosticsSlashCommand {
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let options = Options::parse(arguments);
let options = Options::parse(argument);
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
cx.spawn(move |_| async move {
let Some((text, sections)) = task.await? else {
return Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..1,
icon: IconName::Library,
label: "No Diagnostics".into(),
}],
text: "\n".to_string(),
run_commands_in_text: true,
});
return Ok(SlashCommandOutput::default());
};
let sections = sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
icon: match placeholder_type {
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
PlaceholderType::File(_) => IconName::File,
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => IconName::XCircle,
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
IconName::ExclamationTriangle
}
},
label: match placeholder_type {
PlaceholderType::Root(summary, source) => {
let mut label = String::new();
label.push_str("Diagnostics");
if let Some(source) = source {
write!(label, " ({})", source).unwrap();
}
if summary.error_count > 0 || summary.warning_count > 0 {
label.push(':');
if summary.error_count > 0 {
write!(label, " {} errors", summary.error_count).unwrap();
if summary.warning_count > 0 {
label.push_str(",");
}
}
if summary.warning_count > 0 {
write!(label, " {} warnings", summary.warning_count).unwrap();
}
}
label.into()
}
PlaceholderType::File(file_path) => file_path.into(),
PlaceholderType::Diagnostic(_, message) => message.into(),
},
})
.collect();
Ok(SlashCommandOutput {
text,
sections,
sections: sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
icon: match placeholder_type {
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
PlaceholderType::File(_) => IconName::File,
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
IconName::XCircle
}
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
IconName::ExclamationTriangle
}
},
label: match placeholder_type {
PlaceholderType::Root(summary, source) => {
let mut label = String::new();
label.push_str("Diagnostics");
if let Some(source) = source {
write!(label, " ({})", source).unwrap();
}
if summary.error_count > 0 || summary.warning_count > 0 {
label.push(':');
if summary.error_count > 0 {
write!(label, " {} errors", summary.error_count).unwrap();
if summary.warning_count > 0 {
label.push_str(",");
}
}
if summary.warning_count > 0 {
write!(label, " {} warnings", summary.warning_count)
.unwrap();
}
}
label.into()
}
PlaceholderType::File(file_path) => file_path.into(),
PlaceholderType::Diagnostic(_, message) => message.into(),
},
})
.collect(),
run_commands_in_text: false,
})
})
@@ -249,20 +235,25 @@ struct Options {
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
fn parse(arguments: &[String]) -> Self {
let mut include_warnings = false;
let mut path_matcher = None;
for arg in arguments {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
fn parse(arguments_line: Option<&str>) -> Self {
arguments_line
.map(|arguments_line| {
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
let mut include_warnings = false;
let mut path_matcher = None;
for arg in args {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
})
.unwrap_or_default()
}
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {

View File

@@ -161,28 +161,30 @@ impl SlashCommand for DocsSlashCommand {
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
query: String,
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rust_doc_providers_are_registered(workspace, cx);
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
let args = DocsSlashCommandArgs::parse(arguments);
let args = DocsSlashCommandArgs::parse(&query);
let store = args
.provider()
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_executor().spawn(async move {
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
fn build_completions(
provider: ProviderId,
items: Vec<String>,
) -> Vec<ArgumentCompletion> {
items
.into_iter()
.map(|item| ArgumentCompletion {
label: item.clone().into(),
new_text: item.to_string(),
after_completion: assistant_slash_command::AfterCompletion::Run,
replace_previous_arguments: false,
label: item.clone(),
new_text: format!("{provider} {item}"),
run_command: true,
})
.collect()
}
@@ -192,20 +194,18 @@ impl SlashCommand for DocsSlashCommand {
let providers = indexed_docs_registry.list_providers();
if providers.is_empty() {
return Ok(vec![ArgumentCompletion {
label: "No available docs providers.".into(),
label: "No available docs providers.".to_string(),
new_text: String::new(),
after_completion: false.into(),
replace_previous_arguments: false,
run_command: false,
}]);
}
Ok(providers
.into_iter()
.map(|provider| ArgumentCompletion {
label: provider.to_string().into(),
label: provider.to_string(),
new_text: provider.to_string(),
after_completion: false.into(),
replace_previous_arguments: false,
run_command: false,
})
.collect())
}
@@ -219,48 +219,20 @@ impl SlashCommand for DocsSlashCommand {
if index {
// We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
// until it completes.
drop(store.clone().index(package.as_str().into()));
let _ = store.clone().index(package.as_str().into());
}
let suggested_packages = store.clone().suggest_packages().await?;
let search_results = store.search(package).await;
let mut items = build_completions(search_results);
let workspace_crate_completions = suggested_packages
.into_iter()
.filter(|package_name| {
!items
.iter()
.any(|item| item.label.text() == package_name.as_ref())
})
.map(|package_name| ArgumentCompletion {
label: format!("{package_name} (unindexed)").into(),
new_text: format!("{package_name}"),
after_completion: true.into(),
replace_previous_arguments: false,
})
.collect::<Vec<_>>();
items.extend(workspace_crate_completions);
if items.is_empty() {
return Ok(vec![ArgumentCompletion {
label: format!(
"Enter a {package_term} name.",
package_term = package_term(&provider)
)
.into(),
new_text: provider.to_string(),
after_completion: false.into(),
replace_previous_arguments: false,
}]);
}
Ok(items)
let items = store.search(package).await;
Ok(build_completions(provider, items))
}
DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
DocsSlashCommandArgs::SearchItemDocs {
provider,
item_path,
..
} => {
let store = store?;
let items = store.search(item_path).await;
Ok(build_completions(items))
Ok(build_completions(provider, items))
}
}
})
@@ -268,16 +240,16 @@ impl SlashCommand for DocsSlashCommand {
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing an argument")));
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing argument")));
};
let args = DocsSlashCommandArgs::parse(arguments);
let args = DocsSlashCommandArgs::parse(argument);
let executor = cx.background_executor().clone();
let task = cx.background_executor().spawn({
let store = args
@@ -297,13 +269,6 @@ impl SlashCommand for DocsSlashCommand {
} => (provider, item_path),
};
if key.trim().is_empty() {
bail!(
"no {package_term} name provided",
package_term = package_term(&provider)
);
}
let store = store?;
if let Some(package) = args.package() {
@@ -377,18 +342,12 @@ pub(crate) enum DocsSlashCommandArgs {
}
impl DocsSlashCommandArgs {
pub fn parse(arguments: &[String]) -> Self {
let Some(provider) = arguments
.get(0)
.cloned()
.filter(|arg| !arg.trim().is_empty())
else {
pub fn parse(argument: &str) -> Self {
let Some((provider, argument)) = argument.split_once(' ') else {
return Self::NoProvider;
};
let provider = ProviderId(provider.into());
let Some(argument) = arguments.get(1) else {
return Self::NoProvider;
};
if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
if rest.trim().is_empty() {
@@ -432,15 +391,6 @@ impl DocsSlashCommandArgs {
}
}
/// Returns the term used to refer to a package.
fn package_term(provider: &ProviderId) -> &'static str {
if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() {
return "crate";
}
"package"
}
#[cfg(test)]
mod tests {
use super::*;
@@ -448,16 +398,16 @@ mod tests {
#[test]
fn test_parse_docs_slash_command_args() {
assert_eq!(
DocsSlashCommandArgs::parse(&["".to_string()]),
DocsSlashCommandArgs::parse(""),
DocsSlashCommandArgs::NoProvider
);
assert_eq!(
DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
DocsSlashCommandArgs::parse("rustdoc"),
DocsSlashCommandArgs::NoProvider
);
assert_eq!(
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
DocsSlashCommandArgs::parse("rustdoc "),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "".into(),
@@ -465,7 +415,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
DocsSlashCommandArgs::parse("gleam "),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "".into(),
@@ -474,7 +424,7 @@ mod tests {
);
assert_eq!(
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
DocsSlashCommandArgs::parse("rustdoc gpui"),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -482,7 +432,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
DocsSlashCommandArgs::parse("gleam gleam_stdlib"),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),
@@ -492,7 +442,7 @@ mod tests {
// Adding an item path delimiter indicates we can start indexing.
assert_eq!(
DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
DocsSlashCommandArgs::parse("rustdoc gpui:"),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -500,7 +450,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
DocsSlashCommandArgs::parse("gleam gleam_stdlib/"),
DocsSlashCommandArgs::SearchPackageDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),
@@ -509,10 +459,7 @@ mod tests {
);
assert_eq!(
DocsSlashCommandArgs::parse(&[
"rustdoc".to_string(),
"gpui::foo::bar::Baz".to_string()
]),
DocsSlashCommandArgs::parse("rustdoc gpui::foo::bar::Baz"),
DocsSlashCommandArgs::SearchItemDocs {
provider: ProviderId("rustdoc".into()),
package: "gpui".into(),
@@ -520,10 +467,7 @@ mod tests {
}
);
assert_eq!(
DocsSlashCommandArgs::parse(&[
"gleam".to_string(),
"gleam_stdlib/gleam/int".to_string()
]),
DocsSlashCommandArgs::parse("gleam gleam_stdlib/gleam/int"),
DocsSlashCommandArgs::SearchItemDocs {
provider: ProviderId("gleam".into()),
package: "gleam_stdlib".into(),

View File

@@ -8,7 +8,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use futures::AsyncReadExt;
use gpui::{Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
@@ -117,22 +117,22 @@ impl SlashCommand for FetchSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = arguments.first() else {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing URL")));
};
let Some(workspace) = workspace.upgrade() else {
@@ -150,10 +150,6 @@ impl SlashCommand for FetchSlashCommand {
let url = SharedString::from(url);
cx.foreground_executor().spawn(async move {
let text = text.await?;
if text.trim().is_empty() {
bail!("no textual content found");
}
let range = 0..text.len();
Ok(SlashCommandOutput {
text,

View File

@@ -1,9 +1,9 @@
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task, View, WeakView};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use std::{
fmt::Write,
@@ -12,7 +12,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use util::ResultExt;
use util::{paths::PathMatcher, ResultExt};
use workspace::Workspace;
pub(crate) struct FileSlashCommand;
@@ -29,30 +29,11 @@ impl FileSlashCommand {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.map(|entries| (entries.0, false))
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree.child_entries(Path::new("")).map(move |entry| {
(
project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
},
entry.kind.is_dir(),
)
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
let path_prefix: Arc<str> = "".into();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, is_dir)| {
.filter_map(|(entry, _)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
@@ -63,7 +44,6 @@ impl FileSlashCommand {
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir,
})
})
.collect(),
@@ -74,7 +54,6 @@ impl FileSlashCommand {
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
@@ -122,55 +101,32 @@ impl SlashCommand for FileSlashCommand {
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let paths = self.search_paths(
arguments.last().cloned().unwrap_or_default(),
cancellation_flag,
&workspace,
cx,
);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.filter_map(|path_match| {
.map(|path_match| {
let text = format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
);
let mut label = CodeLabel::default();
let file_name = path_match.path.file_name()?.to_string_lossy();
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
} else {
format!("{} ", file_name)
};
label.push_str(label_text.as_str(), None);
label.push_str(&text, comment_id);
label.filter_range = 0..file_name.len();
Some(ArgumentCompletion {
label,
ArgumentCompletion {
label: text.clone(),
new_text: text,
after_completion: if path_match.is_dir {
AfterCompletion::Compose
} else {
AfterCompletion::Run
},
replace_previous_arguments: false,
})
run_command: true,
}
})
.collect())
})
@@ -178,33 +134,32 @@ impl SlashCommand for FileSlashCommand {
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
if arguments.is_empty() {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing path")));
};
let task = collect_files(workspace.read(cx).project().clone(), arguments, cx);
let task = collect_files(workspace.read(cx).project().clone(), argument, cx);
cx.foreground_executor().spawn(async move {
let output = task.await?;
let (text, ranges) = task.await?;
Ok(SlashCommandOutput {
text: output.completion_text,
sections: output
.files
text,
sections: ranges
.into_iter()
.map(|file| {
.map(|(range, path, entry_type)| {
build_entry_output_section(
file.range_in_text,
Some(&file.path),
file.entry_type == EntryType::Directory,
range,
Some(&path),
entry_type == EntryType::Directory,
None,
)
})
@@ -215,38 +170,18 @@ impl SlashCommand for FileSlashCommand {
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[derive(Clone, Copy, PartialEq)]
enum EntryType {
File,
Directory,
}
#[derive(Clone, PartialEq, Debug)]
struct FileCommandOutput {
completion_text: String,
files: Vec<OutputFile>,
}
#[derive(Clone, PartialEq, Debug)]
struct OutputFile {
range_in_text: Range<usize>,
path: PathBuf,
entry_type: EntryType,
}
fn collect_files(
project: Model<Project>,
glob_inputs: &[String],
glob_input: &str,
cx: &mut AppContext,
) -> Task<Result<FileCommandOutput>> {
let Ok(matchers) = glob_inputs
.into_iter()
.map(|glob_input| {
custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()])
.with_context(|| format!("invalid path {glob_input}"))
})
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
else {
) -> Task<Result<(String, Vec<(Range<usize>, PathBuf, EntryType)>)>> {
let Ok(matcher) = PathMatcher::new(&[glob_input.to_owned()]) else {
return Task::ready(Err(anyhow!("invalid path")));
};
@@ -256,7 +191,6 @@ fn collect_files(
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>();
cx.spawn(|mut cx| async move {
let mut text = String::new();
let mut ranges = Vec::new();
@@ -265,16 +199,11 @@ fn collect_files(
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
let mut folded_directory_names_stack = Vec::new();
let mut is_top_level_directory = true;
for entry in snapshot.entries(false, 0) {
let mut path_including_worktree_name = PathBuf::new();
path_including_worktree_name.push(snapshot.root_name());
path_including_worktree_name.push(&entry.path);
if !matchers
.iter()
.any(|matcher| matcher.is_match(&path_including_worktree_name))
{
if !matcher.is_match(&path_including_worktree_name) {
continue;
}
@@ -283,11 +212,11 @@ fn collect_files(
break;
}
let (_, entry_name, start) = directory_stack.pop().unwrap();
ranges.push(OutputFile {
range_in_text: start..text.len().saturating_sub(1),
path: PathBuf::from(entry_name),
entry_type: EntryType::Directory,
});
ranges.push((
start..text.len().saturating_sub(1),
PathBuf::from(entry_name),
EntryType::Directory,
));
}
let filename = entry
@@ -344,55 +273,35 @@ fn collect_files(
continue;
};
if let Some(buffer) = open_buffer_task.await.log_err() {
let buffer_snapshot =
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
let prev_len = text.len();
collect_file_content(
&mut text,
&buffer_snapshot,
path_including_worktree_name.to_string_lossy().to_string(),
);
collect_file_content(&mut text, &snapshot, filename.clone());
text.push('\n');
if !write_single_file_diagnostics(
&mut text,
Some(&path_including_worktree_name),
&buffer_snapshot,
&snapshot,
) {
text.pop();
}
ranges.push(OutputFile {
range_in_text: prev_len..text.len(),
path: path_including_worktree_name,
entry_type: EntryType::File,
});
ranges.push((
prev_len..text.len(),
PathBuf::from(filename),
EntryType::File,
));
text.push('\n');
}
}
}
while let Some((dir, entry, start)) = directory_stack.pop() {
if directory_stack.is_empty() {
let mut root_path = PathBuf::new();
root_path.push(snapshot.root_name());
root_path.push(&dir);
ranges.push(OutputFile {
range_in_text: start..text.len(),
path: root_path,
entry_type: EntryType::Directory,
});
} else {
ranges.push(OutputFile {
range_in_text: start..text.len(),
path: PathBuf::from(entry.as_str()),
entry_type: EntryType::Directory,
});
}
while let Some((dir, _, start)) = directory_stack.pop() {
let mut root_path = PathBuf::new();
root_path.push(snapshot.root_name());
root_path.push(&dir);
ranges.push((start..text.len(), root_path, EntryType::Directory));
}
}
Ok(FileCommandOutput {
completion_text: text,
files: ranges,
})
Ok((text, ranges))
})
}
@@ -460,300 +369,3 @@ pub fn build_entry_output_section(
label: label.into(),
}
}
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
/// check. Only subpaths pass the prefix check, rather than any prefix.
mod custom_path_matcher {
use std::{fmt::Debug as _, path::Path};
use globset::{Glob, GlobSet, GlobSetBuilder};
#[derive(Clone, Debug, Default)]
pub struct PathMatcher {
sources: Vec<String>,
sources_with_trailing_slash: Vec<String>,
glob: GlobSet,
}
impl std::fmt::Display for PathMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.sources.fmt(f)
}
}
impl PartialEq for PathMatcher {
fn eq(&self, other: &Self) -> bool {
self.sources.eq(&other.sources)
}
}
impl Eq for PathMatcher {}
impl PathMatcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.into_iter()
.map(|glob| Glob::new(&glob))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs
.iter()
.map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
.collect();
let mut glob_builder = GlobSetBuilder::new();
for single_glob in globs {
glob_builder.add(single_glob);
}
let glob = glob_builder.build()?;
Ok(PathMatcher {
glob,
sources,
sources_with_trailing_slash,
})
}
pub fn sources(&self) -> &[String] {
&self.sources
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
let other_path = other.as_ref();
self.sources
.iter()
.zip(self.sources_with_trailing_slash.iter())
.any(|(source, with_slash)| {
let as_bytes = other_path.as_os_str().as_encoded_bytes();
let with_slash = if source.ends_with("/") {
source.as_bytes()
} else {
with_slash.as_bytes()
};
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
})
|| self.glob.is_match(other_path)
|| self.check_with_end_separator(other_path)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let separator = std::path::MAIN_SEPARATOR_STR;
if path_str.ends_with(separator) {
return false;
} else {
self.glob.is_match(path_str.to_string() + separator)
}
}
}
}
#[cfg(test)]
mod test {
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use crate::slash_command::file_command::collect_files;
pub fn init_test(cx: &mut gpui::TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
// release_channel::init(SemanticVersion::default(), cx);
language::init(cx);
Project::init_settings(cx);
});
}
#[gpui::test]
async fn test_file_exact_matching(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"file_0": "0"
},
"file_1": "1",
"file_2": "2",
"file_3": "3",
},
"dir.rs": "4"
}),
)
.await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let result_1 = cx
.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx))
.await
.unwrap();
assert!(result_1.completion_text.starts_with("root/dir"));
// 4 files + 2 directories
assert_eq!(6, result_1.files.len());
let result_2 = cx
.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
.await
.unwrap();
assert_eq!(result_1, result_2);
let result = cx
.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx))
.await
.unwrap();
assert!(result.completion_text.starts_with("root/dir"));
// 5 files + 2 directories
assert_eq!(7, result.files.len());
// Ensure that the project lasts until after the last await
drop(project);
}
#[gpui::test]
async fn test_file_sub_directory_rendering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/zed",
json!({
"assets": {
"dir1": {
".gitkeep": ""
},
"dir2": {
".gitkeep": ""
},
"themes": {
"ayu": {
"LICENSE": "1",
},
"andromeda": {
"LICENSE": "2",
},
"summercamp": {
"LICENSE": "3",
},
},
},
}),
)
.await;
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
let result = cx
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
.await
.unwrap();
// Sanity check
assert!(result.completion_text.starts_with("zed/assets/themes\n"));
assert_eq!(7, result.files.len());
// Ensure that full file paths are included in the real output
assert!(result
.completion_text
.contains("zed/assets/themes/andromeda/LICENSE"));
assert!(result
.completion_text
.contains("zed/assets/themes/ayu/LICENSE"));
assert!(result
.completion_text
.contains("zed/assets/themes/summercamp/LICENSE"));
assert_eq!("summercamp", result.files[5].path.to_string_lossy());
// Ensure that things are in descending order, with properly relativized paths
assert_eq!(
"zed/assets/themes/andromeda/LICENSE",
result.files[0].path.to_string_lossy()
);
assert_eq!("andromeda", result.files[1].path.to_string_lossy());
assert_eq!(
"zed/assets/themes/ayu/LICENSE",
result.files[2].path.to_string_lossy()
);
assert_eq!("ayu", result.files[3].path.to_string_lossy());
assert_eq!(
"zed/assets/themes/summercamp/LICENSE",
result.files[4].path.to_string_lossy()
);
// Ensure that the project lasts until after the last await
drop(project);
}
#[gpui::test]
async fn test_file_deep_sub_directory_rendering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/zed",
json!({
"assets": {
"themes": {
"LICENSE": "1",
"summercamp": {
"LICENSE": "1",
"subdir": {
"LICENSE": "1",
"subsubdir": {
"LICENSE": "3",
}
}
},
},
},
}),
)
.await;
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
let result = cx
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
.await
.unwrap();
assert!(result.completion_text.starts_with("zed/assets/themes\n"));
assert_eq!(
"zed/assets/themes/LICENSE",
result.files[0].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/LICENSE",
result.files[1].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/subdir/LICENSE",
result.files[2].path.to_string_lossy()
);
assert_eq!(
"zed/assets/themes/summercamp/subdir/subsubdir/LICENSE",
result.files[3].path.to_string_lossy()
);
assert_eq!("subsubdir", result.files[4].path.to_string_lossy());
assert_eq!("subdir", result.files[5].path.to_string_lossy());
assert_eq!("summercamp", result.files[6].path.to_string_lossy());
assert_eq!("zed/assets/themes", result.files[7].path.to_string_lossy());
// Ensure that the project lasts until after the last await
drop(project);
}
}

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use chrono::Local;
use gpui::{Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use ui::prelude::*;
use workspace::Workspace;
@@ -32,19 +32,19 @@ impl SlashCommand for NowSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
_cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let now = Local::now();

View File

@@ -103,10 +103,10 @@ impl SlashCommand for ProjectSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -117,9 +117,9 @@ impl SlashCommand for ProjectSlashCommand {
fn run(
self: Arc<Self>,
_arguments: &[String],
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
use ui::prelude::*;
@@ -29,13 +29,12 @@ impl SlashCommand for PromptSlashCommand {
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
cx.background_executor().spawn(async move {
let prompts = store.await?.search(query).await;
Ok(prompts
@@ -43,10 +42,9 @@ impl SlashCommand for PromptSlashCommand {
.filter_map(|prompt| {
let prompt_title = prompt.title?.to_string();
Some(ArgumentCompletion {
label: prompt_title.clone().into(),
label: prompt_title.clone(),
new_text: prompt_title,
after_completion: true.into(),
replace_previous_arguments: true,
run_command: true,
})
})
.collect())
@@ -55,18 +53,17 @@ impl SlashCommand for PromptSlashCommand {
fn run(
self: Arc<Self>,
arguments: &[String],
title: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let title = arguments.to_owned().join(" ");
if title.trim().is_empty() {
let Some(title) = title else {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let store = PromptStore::global(cx);
let title = SharedString::from(title.clone());
let title = SharedString::from(title.to_string());
let prompt = cx.background_executor().spawn({
let title = title.clone();
async move {
@@ -80,11 +77,6 @@ impl SlashCommand for PromptSlashCommand {
});
cx.foreground_executor().spawn(async move {
let mut prompt = prompt.await?;
if prompt.starts_with('/') {
// Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
prompt.insert(0, '\n');
}
if prompt.is_empty() {
prompt.push('\n');
}

View File

@@ -5,7 +5,6 @@ use super::{
};
use anyhow::Result;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use feature_flags::FeatureFlag;
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
use semantic_index::SemanticIndex;
@@ -18,12 +17,6 @@ use ui::{prelude::*, IconName};
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct SearchSlashCommandFeatureFlag;
impl FeatureFlag for SearchSlashCommandFeatureFlag {
const NAME: &'static str = "search-slash-command";
}
pub(crate) struct SearchSlashCommand;
impl SlashCommand for SearchSlashCommand {
@@ -49,31 +42,31 @@ impl SlashCommand for SearchSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
if arguments.is_empty() {
let Some(argument) = argument else {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
};
let mut limit = None;
let mut query = String::new();
for part in arguments {
for part in argument.split(' ') {
if let Some(parameter) = part.strip_prefix("--") {
if let Ok(count) = parameter.parse::<usize>() {
limit = Some(count);

View File

@@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use editor::Editor;
use gpui::{Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
@@ -26,10 +26,10 @@ impl SlashCommand for OutlineSlashCommand {
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -40,9 +40,9 @@ impl SlashCommand for OutlineSlashCommand {
fn run(
self: Arc<Self>,
_arguments: &[String],
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {

View File

@@ -1,321 +0,0 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{Context, Result};
use assistant_slash_command::ArgumentCompletion;
use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Entity, Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct TabSlashCommand;
const ALL_TABS_COMPLETION_ITEM: &str = "all";
impl SlashCommand for TabSlashCommand {
fn name(&self) -> String {
"tab".into()
}
fn description(&self) -> String {
"insert open tabs (active tab by default)".to_owned()
}
fn menu_text(&self) -> String {
"Insert Open Tabs".to_owned()
}
fn requires_argument(&self) -> bool {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let mut has_all_tabs_completion_item = false;
let argument_set = arguments
.iter()
.filter(|argument| {
if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
has_all_tabs_completion_item = true;
false
} else {
true
}
})
.cloned()
.collect::<HashSet<_>>();
if has_all_tabs_completion_item {
return Task::ready(Ok(Vec::new()));
}
let active_item_path = workspace.as_ref().and_then(|workspace| {
workspace
.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
snapshot.resolve_file_path(cx, true)
})
.ok()
.flatten()
});
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
cx.spawn(|_| async move {
let tab_items = tab_items_search.await?;
let run_command = tab_items.len() == 1;
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
let path_string = path.as_deref()?.to_string_lossy().to_string();
if argument_set.contains(&path_string) {
return None;
}
if active_item_path.is_some() && active_item_path == path {
return None;
}
Some(ArgumentCompletion {
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
})
});
let active_item_completion = active_item_path
.as_deref()
.map(|active_item_path| active_item_path.to_string_lossy().to_string())
.filter(|path_string| !argument_set.contains(path_string))
.map(|path_string| ArgumentCompletion {
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
});
Ok(active_item_completion
.into_iter()
.chain(Some(ArgumentCompletion {
label: ALL_TABS_COMPLETION_ITEM.into(),
new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
replace_previous_arguments: false,
after_completion: true.into(),
}))
.chain(tab_completion_items)
.collect())
})
}
fn run(
self: Arc<Self>,
arguments: &[String],
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let tab_items_search = tab_items_for_queries(
Some(workspace),
arguments,
Arc::new(AtomicBool::new(false)),
true,
cx,
);
cx.background_executor().spawn(async move {
let mut sections = Vec::new();
let mut text = String::new();
let mut has_diagnostics = false;
for (full_path, buffer, _) in tab_items_search.await? {
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```").unwrap();
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
has_diagnostics = true;
}
if !text.ends_with('\n') {
text.push('\n');
}
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
full_path.as_deref(),
false,
None,
));
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: has_diagnostics,
})
})
}
}
fn tab_items_for_queries(
workspace: Option<WeakView<Workspace>>,
queries: &[String],
cancel: Arc<AtomicBool>,
strict_match: bool,
cx: &mut WindowContext,
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
let queries = queries.to_owned();
cx.spawn(|mut cx| async move {
let mut open_buffers =
workspace
.context("no workspace")?
.update(&mut cx, |workspace, cx| {
if strict_match && empty_query {
let snapshot = active_item_buffer(workspace, cx)?;
let full_path = snapshot.resolve_file_path(cx, true);
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
}
let mut timestamps_by_entity_id = HashMap::default();
let mut visited_buffers = HashSet::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) =
timestamps_by_entity_id.get(&editor.entity_id())
{
if visited_buffers.insert(buffer.read(cx).remote_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
}
Ok(open_buffers)
})??;
let background_executor = cx.background_executor().clone();
cx.background_executor()
.spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
if empty_query
|| queries
.iter()
.any(|query| query == ALL_TABS_COMPLETION_ITEM)
{
return Ok(open_buffers);
}
let matched_items = if strict_match {
let match_candidates = open_buffers
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some((id, path_string))
})
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
candidates
.entry(path_string)
.or_insert_with(|| Vec::new())
.push(id);
candidates
});
queries
.iter()
.filter_map(|query| match_candidates.get(query))
.flatten()
.copied()
.filter_map(|id| open_buffers.get(id))
.cloned()
.collect()
} else {
let match_candidates = open_buffers
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some(fuzzy::StringMatchCandidate {
id,
char_bag: path_string.as_str().into(),
string: path_string,
})
})
.collect::<Vec<_>>();
let mut processed_matches = HashSet::default();
let file_queries = queries.iter().map(|query| {
fuzzy::match_strings(
&match_candidates,
query,
true,
usize::MAX,
&cancel,
background_executor.clone(),
)
});
join_all(file_queries)
.await
.into_iter()
.flatten()
.filter(|string_match| processed_matches.insert(string_match.candidate_id))
.filter_map(|string_match| open_buffers.get(string_match.candidate_id))
.cloned()
.collect()
};
Ok(matched_items)
})
.await
})
}
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ui::ViewContext<Workspace>,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)
.context("no active item")?
.downcast::<Editor>()
.context("active item is not an editor")?;
let snapshot = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.context("active editor is not a singleton buffer")?
.read(cx)
.snapshot();
Ok(snapshot)
}

View File

@@ -0,0 +1,118 @@
use super::{
diagnostics_command::write_single_file_diagnostics,
file_command::{build_entry_output_section, codeblock_fence_for_path},
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
use language::LspAdapterDelegate;
use std::{fmt::Write, sync::Arc};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct TabsSlashCommand;
impl SlashCommand for TabsSlashCommand {
fn name(&self) -> String {
"tabs".into()
}
fn description(&self) -> String {
"insert open tabs".into()
}
fn menu_text(&self) -> String {
"Insert Open Tabs".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_query: String,
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let open_buffers = workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
open_buffers
});
match open_buffers {
Ok(mut open_buffers) => cx.background_executor().spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
let mut sections = Vec::new();
let mut text = String::new();
let mut has_diagnostics = false;
for (full_path, buffer, _) in open_buffers {
let section_start_ix = text.len();
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```").unwrap();
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
has_diagnostics = true;
}
if !text.ends_with('\n') {
text.push('\n');
}
let section_end_ix = text.len() - 1;
sections.push(build_entry_output_section(
section_start_ix..section_end_ix,
full_path.as_deref(),
false,
None,
));
}
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: has_diagnostics,
})
}),
Err(error) => Task::ready(Err(error)),
}
}
}

View File

@@ -5,27 +5,25 @@ use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, Task, View, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::prelude::*;
use workspace::{dock::Panel, Workspace};
use crate::DEFAULT_CONTEXT_LINES;
use workspace::Workspace;
use super::create_label_for_command;
pub(crate) struct TerminalSlashCommand;
pub(crate) struct TermSlashCommand;
const LINE_COUNT_ARG: &str = "--line-count";
impl SlashCommand for TerminalSlashCommand {
impl SlashCommand for TermSlashCommand {
fn name(&self) -> String {
"terminal".into()
"term".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
create_label_for_command("terminal", &[LINE_COUNT_ARG], cx)
create_label_for_command("term", &[LINE_COUNT_ARG], cx)
}
fn description(&self) -> String {
@@ -40,39 +38,44 @@ impl SlashCommand for TerminalSlashCommand {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
Task::ready(Ok(vec![ArgumentCompletion {
label: LINE_COUNT_ARG.to_string(),
new_text: LINE_COUNT_ARG.to_string(),
run_command: true,
}]))
}
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
};
let Some(active_terminal) = terminal_panel
.read(cx)
.pane()
.read(cx)
.active_item()
.and_then(|t| t.downcast::<TerminalView>())
else {
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
};
let line_count = arguments
.get(0)
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_CONTEXT_LINES);
let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20);
let lines = active_terminal
.read(cx)
@@ -97,22 +100,12 @@ impl SlashCommand for TerminalSlashCommand {
}
}
fn resolve_active_terminal(
workspace: &View<Workspace>,
cx: &WindowContext,
) -> Option<View<TerminalView>> {
if let Some(terminal_view) = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.act_as::<TerminalView>(cx))
{
return Some(terminal_view);
fn parse_argument(argument: &str) -> Option<usize> {
let mut args = argument.split(' ');
if args.next() == Some(LINE_COUNT_ARG) {
if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
return Some(line_count);
}
}
let terminal_panel = workspace.read(cx).panel::<TerminalPanel>(cx)?;
terminal_panel.read(cx).pane().and_then(|pane| {
pane.read(cx)
.active_item()
.and_then(|t| t.downcast::<TerminalView>())
})
None
}

View File

@@ -1,76 +0,0 @@
use crate::prompts::PromptBuilder;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{Task, WeakView};
use language::LspAdapterDelegate;
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct WorkflowSlashCommand {
prompt_builder: Arc<PromptBuilder>,
}
impl WorkflowSlashCommand {
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
Self { prompt_builder }
}
}
impl SlashCommand for WorkflowSlashCommand {
fn name(&self) -> String {
"workflow".into()
}
fn description(&self) -> String {
"insert a prompt that opts into the edit workflow".into()
}
fn menu_text(&self) -> String {
"Insert Workflow Prompt".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|_cx| async move {
let text = prompt_builder.generate_workflow_prompt()?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Route,
label: "Workflow".into(),
}],
run_commands_in_text: false,
})
})
}
}

View File

@@ -1,306 +0,0 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
}
#[derive(Clone)]
struct SlashCommandInfo {
name: SharedString,
description: SharedString,
args: Option<SharedString>,
}
#[derive(Clone)]
enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
}
impl AsRef<str> for SlashCommandEntry {
fn as_ref(&self) -> &str {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
}
}
}
pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandEntry>,
active_context_editor: WeakView<ContextEditor>,
selected_index: usize,
}
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
registry,
active_context_editor,
trigger,
}
}
}
impl PickerDelegate for SlashCommandDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_commands.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a command...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_commands = self.all_commands.clone();
cx.spawn(|this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_commands
} else {
all_commands
.into_iter()
.filter(|model_info| {
model_info
.as_ref()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.filtered_commands = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
})
.ok();
})
}
fn separators_after_indices(&self) -> Vec<usize> {
let mut ret = vec![];
let mut previous_is_advert = false;
for (index, command) in self.filtered_commands.iter().enumerate() {
if previous_is_advert {
if let SlashCommandEntry::Info(_) = command {
previous_is_advert = false;
debug_assert_ne!(
index, 0,
"index cannot be zero, as we can never have a separator at 0th position"
);
ret.push(index - 1);
}
} else {
if let SlashCommandEntry::Advert { .. } = command {
previous_is_advert = true;
if index != 0 {
ret.push(index - 1);
}
}
}
}
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
if let SlashCommandEntry::Info(info) = command {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
match command_info {
SlashCommandEntry::Info(info) => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
h_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(220.))
.child(
v_flex()
.child(
h_flex()
.child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name);
if let Some(args) =
info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args).size(LabelSize::Small),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
),
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(renderer(cx)),
),
}
}
}
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let all_models = self
.registry
.featured_command_names()
.into_iter()
.filter_map(|command_name| {
let command = self.registry.command(&command_name)?;
let menu_text = SharedString::from(Arc::from(command.menu_text()));
let label = command.label(cx);
let args = label.filter_range.end.ne(&label.text.len()).then(|| {
SharedString::from(
label.text[label.filter_range.end..label.text.len()].to_owned(),
)
});
Some(SlashCommandEntry::Info(SlashCommandInfo {
name: command_name.into(),
description: menu_text,
args,
}))
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.child(
h_flex()
.font_buffer(cx)
.items_center()
.gap_1()
.child(div().font_buffer(cx).child(
Label::new("create-your-command").size(LabelSize::Small),
))
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
)
.child(
Label::new("Learn how to create a custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {
all_commands: all_models.clone(),
active_context_editor: self.active_context_editor.clone(),
filtered_commands: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
let handle = self
.active_context_editor
.update(cx, |this, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -1,44 +0,0 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct SlashCommandSettings {
/// Settings for the `/docs` slash command.
#[serde(default)]
pub docs: DocsCommandSettings,
/// Settings for the `/project` slash command.
#[serde(default)]
pub project: ProjectCommandSettings,
}
/// Settings for the `/docs` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct DocsCommandSettings {
/// Whether `/docs` is enabled.
#[serde(default)]
pub enabled: bool,
}
/// Settings for the `/project` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct ProjectCommandSettings {
/// Whether `/project` is enabled.
#[serde(default)]
pub enabled: bool,
}
impl Settings for SlashCommandSettings {
const KEY: Option<&'static str> = Some("slash_commands");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default].into_iter().chain(sources.user),
)
}
}

View File

@@ -1,6 +1,5 @@
use collections::HashMap;
use ordered_float::OrderedFloat;
use rope::{Point, Rope, TextSummary};
use std::collections::{BTreeSet, HashMap};
use std::{
cmp,
fmt::{self, Debug},
@@ -65,11 +64,11 @@ impl Debug for Matrix {
}
}
#[derive(Debug, Clone)]
pub enum CharOperation {
#[derive(Debug)]
pub enum Hunk {
Insert { text: String },
Delete { bytes: usize },
Keep { bytes: usize },
Remove { len: usize },
Keep { len: usize },
}
pub struct StreamingDiff {
@@ -104,7 +103,7 @@ impl StreamingDiff {
}
}
pub fn push_new(&mut self, text: &str) -> Vec<CharOperation> {
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
self.new.extend(text.chars());
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
@@ -146,7 +145,7 @@ impl StreamingDiff {
hunks
}
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<CharOperation> {
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
let mut pending_insert: Option<Range<usize>> = None;
let mut hunks = Vec::new();
let mut i = old_text_ix;
@@ -186,22 +185,22 @@ impl StreamingDiff {
}
} else {
if let Some(range) = pending_insert.take() {
hunks.push(CharOperation::Insert {
hunks.push(Hunk::Insert {
text: self.new[range].iter().collect(),
});
}
let char_len = self.old[i - 1].len_utf8();
if prev_i == i - 1 && prev_j == j {
if let Some(CharOperation::Delete { bytes: len }) = hunks.last_mut() {
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
*len += char_len;
} else {
hunks.push(CharOperation::Delete { bytes: char_len })
hunks.push(Hunk::Remove { len: char_len })
}
} else if let Some(CharOperation::Keep { bytes: len }) = hunks.last_mut() {
} else if let Some(Hunk::Keep { len }) = hunks.last_mut() {
*len += char_len;
} else {
hunks.push(CharOperation::Keep { bytes: char_len })
hunks.push(Hunk::Keep { len: char_len })
}
}
@@ -210,7 +209,7 @@ impl StreamingDiff {
}
if let Some(range) = pending_insert.take() {
hunks.push(CharOperation::Insert {
hunks.push(Hunk::Insert {
text: self.new[range].iter().collect(),
});
}
@@ -219,853 +218,74 @@ impl StreamingDiff {
hunks
}
pub fn finish(self) -> Vec<CharOperation> {
pub fn finish(self) -> Vec<Hunk> {
self.backtrack(self.old.len(), self.new.len())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LineOperation {
Insert { lines: u32 },
Delete { lines: u32 },
Keep { lines: u32 },
}
#[derive(Debug, Default)]
pub struct LineDiff {
inserted_newline_at_end: bool,
/// The extent of kept and deleted text.
old_end: Point,
/// The extent of kept and inserted text.
new_end: Point,
/// Deleted rows, expressed in terms of the old text.
deleted_rows: BTreeSet<u32>,
/// Inserted rows, expressed in terms of the new text.
inserted_rows: BTreeSet<u32>,
buffered_insert: String,
/// After deleting a newline, we buffer deletion until we keep or insert a character.
buffered_delete: usize,
}
impl LineDiff {
pub fn push_char_operations<'a>(
&mut self,
operations: impl IntoIterator<Item = &'a CharOperation>,
old_text: &Rope,
) {
for operation in operations {
self.push_char_operation(operation, old_text);
}
}
pub fn push_char_operation(&mut self, operation: &CharOperation, old_text: &Rope) {
match operation {
CharOperation::Insert { text } => {
self.flush_delete(old_text);
if is_line_start(self.old_end) {
if let Some(newline_ix) = text.rfind('\n') {
let (prefix, suffix) = text.split_at(newline_ix + 1);
self.buffered_insert.push_str(prefix);
self.flush_insert(old_text);
self.buffered_insert.push_str(suffix);
} else {
self.buffered_insert.push_str(&text);
}
} else {
self.buffered_insert.push_str(&text);
if !text.ends_with('\n') {
self.flush_insert(old_text);
}
}
}
CharOperation::Delete { bytes } => {
self.buffered_delete += bytes;
let common_suffix_len = self.trim_buffered_end(old_text);
self.flush_insert(old_text);
if common_suffix_len > 0 || !is_line_end(self.old_end, old_text) {
self.flush_delete(old_text);
self.keep(common_suffix_len, old_text);
}
}
CharOperation::Keep { bytes } => {
self.flush_delete(old_text);
self.flush_insert(old_text);
self.keep(*bytes, old_text);
}
}
}
fn flush_insert(&mut self, old_text: &Rope) {
if self.buffered_insert.is_empty() {
return;
}
let new_start = self.new_end;
let lines = TextSummary::from(self.buffered_insert.as_str()).lines;
self.new_end += lines;
if is_line_start(self.old_end) {
if self.new_end.column == 0 {
self.inserted_rows.extend(new_start.row..self.new_end.row);
} else {
self.deleted_rows.insert(self.old_end.row);
self.inserted_rows.extend(new_start.row..=self.new_end.row);
}
} else if is_line_end(self.old_end, old_text) {
if self.buffered_insert.starts_with('\n') {
self.inserted_rows
.extend(new_start.row + 1..=self.new_end.row);
self.inserted_newline_at_end = true;
} else {
if !self.inserted_newline_at_end {
self.deleted_rows.insert(self.old_end.row);
}
self.inserted_rows.extend(new_start.row..=self.new_end.row);
}
} else {
self.deleted_rows.insert(self.old_end.row);
self.inserted_rows.extend(new_start.row..=self.new_end.row);
}
self.buffered_insert.clear();
}
fn flush_delete(&mut self, old_text: &Rope) {
if self.buffered_delete == 0 {
return;
}
let old_start = self.old_end;
self.old_end =
old_text.offset_to_point(old_text.point_to_offset(self.old_end) + self.buffered_delete);
if is_line_end(old_start, old_text) && is_line_end(self.old_end, old_text) {
self.deleted_rows
.extend(old_start.row + 1..=self.old_end.row);
} else if is_line_start(old_start)
&& (is_line_start(self.old_end) && self.old_end < old_text.max_point())
&& self.new_end.column == 0
{
self.deleted_rows.extend(old_start.row..self.old_end.row);
} else {
self.inserted_rows.insert(self.new_end.row);
self.deleted_rows.extend(old_start.row..=self.old_end.row);
}
self.inserted_newline_at_end = false;
self.buffered_delete = 0;
}
fn keep(&mut self, bytes: usize, old_text: &Rope) {
if bytes == 0 {
return;
}
let lines =
old_text.offset_to_point(old_text.point_to_offset(self.old_end) + bytes) - self.old_end;
self.old_end += lines;
self.new_end += lines;
self.inserted_newline_at_end = false;
}
fn trim_buffered_end(&mut self, old_text: &Rope) -> usize {
let old_start_offset = old_text.point_to_offset(self.old_end);
let old_end_offset = old_start_offset + self.buffered_delete;
let new_chars = self.buffered_insert.chars().rev();
let old_chars = old_text
.chunks_in_range(old_start_offset..old_end_offset)
.flat_map(|chunk| chunk.chars().rev());
let mut common_suffix_len = 0;
for (new_ch, old_ch) in new_chars.zip(old_chars) {
if new_ch == old_ch {
common_suffix_len += new_ch.len_utf8();
} else {
break;
}
}
self.buffered_delete -= common_suffix_len;
self.buffered_insert
.truncate(self.buffered_insert.len() - common_suffix_len);
common_suffix_len
}
pub fn finish(&mut self, old_text: &Rope) {
self.flush_insert(old_text);
self.flush_delete(old_text);
let old_start = self.old_end;
self.old_end = old_text.max_point();
self.new_end += self.old_end - old_start;
}
pub fn line_operations(&self) -> Vec<LineOperation> {
let mut ops = Vec::new();
let mut deleted_rows = self.deleted_rows.iter().copied().peekable();
let mut inserted_rows = self.inserted_rows.iter().copied().peekable();
let mut old_row = 0;
let mut new_row = 0;
while deleted_rows.peek().is_some() || inserted_rows.peek().is_some() {
// Check for a run of deleted lines at current old row.
if Some(old_row) == deleted_rows.peek().copied() {
if let Some(LineOperation::Delete { lines }) = ops.last_mut() {
*lines += 1;
} else {
ops.push(LineOperation::Delete { lines: 1 });
}
old_row += 1;
deleted_rows.next();
} else if Some(new_row) == inserted_rows.peek().copied() {
if let Some(LineOperation::Insert { lines }) = ops.last_mut() {
*lines += 1;
} else {
ops.push(LineOperation::Insert { lines: 1 });
}
new_row += 1;
inserted_rows.next();
} else {
// Keep lines until the next deletion, insertion, or the end of the old text.
let lines_to_next_deletion = inserted_rows
.peek()
.copied()
.unwrap_or(self.new_end.row + 1)
- new_row;
let lines_to_next_insertion =
deleted_rows.peek().copied().unwrap_or(self.old_end.row + 1) - old_row;
let kept_lines =
cmp::max(1, cmp::min(lines_to_next_insertion, lines_to_next_deletion));
if kept_lines > 0 {
ops.push(LineOperation::Keep { lines: kept_lines });
old_row += kept_lines;
new_row += kept_lines;
}
}
}
if old_row < self.old_end.row + 1 {
ops.push(LineOperation::Keep {
lines: self.old_end.row + 1 - old_row,
});
}
ops
}
}
fn is_line_start(point: Point) -> bool {
point.column == 0
}
fn is_line_end(point: Point, text: &Rope) -> bool {
text.line_len(point.row) == point.column
}
#[cfg(test)]
mod tests {
use super::*;
use rand::prelude::*;
use std::env;
#[test]
fn test_delete_first_of_two_lines() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Delete { bytes: 5 },
CharOperation::Keep { bytes: 4 },
];
let expected_line_ops = vec![
LineOperation::Delete { lines: 1 },
LineOperation::Keep { lines: 1 },
];
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &expected_line_ops)
);
use super::*;
use rand::prelude::*;
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(line_ops, expected_line_ops);
}
#[gpui::test(iterations = 100)]
fn test_random_diffs(mut rng: StdRng) {
let old_text_len = env::var("OLD_TEXT_LEN")
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
.unwrap_or(10);
let new_text_len = env::var("NEW_TEXT_LEN")
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
.unwrap_or(10);
#[test]
fn test_delete_second_of_two_lines() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 5 },
CharOperation::Delete { bytes: 4 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
let old = util::RandomCharIter::new(&mut rng)
.take(old_text_len)
.collect::<String>();
log::info!("old text: {:?}", old);
#[test]
fn test_add_new_line() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 9 },
CharOperation::Insert {
text: "\ncccc".into(),
},
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 2 },
LineOperation::Insert { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_delete_line_in_middle() {
let old_text = "aaaa\nbbbb\ncccc";
let char_ops = vec![
CharOperation::Keep { bytes: 5 },
CharOperation::Delete { bytes: 5 },
CharOperation::Keep { bytes: 4 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 1 },
LineOperation::Keep { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_replace_line() {
let old_text = "aaaa\nbbbb\ncccc";
let char_ops = vec![
CharOperation::Keep { bytes: 5 },
CharOperation::Delete { bytes: 4 },
CharOperation::Insert {
text: "BBBB".into(),
},
CharOperation::Keep { bytes: 5 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_multiple_edits_on_different_lines() {
let old_text = "aaaa\nbbbb\ncccc\ndddd";
let char_ops = vec![
CharOperation::Insert { text: "A".into() },
CharOperation::Keep { bytes: 9 },
CharOperation::Delete { bytes: 5 },
CharOperation::Keep { bytes: 4 },
CharOperation::Insert {
text: "\nEEEE".into(),
},
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 2 },
LineOperation::Insert { lines: 2 },
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_edit_at_end_of_line() {
let old_text = "aaaa\nbbbb\ncccc";
let char_ops = vec![
CharOperation::Keep { bytes: 4 },
CharOperation::Insert { text: "A".into() },
CharOperation::Keep { bytes: 10 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 2 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_insert_newline_character() {
let old_text = "aaaabbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 4 },
CharOperation::Insert { text: "\n".into() },
CharOperation::Keep { bytes: 4 },
];
let new_text = apply_char_operations(old_text, &char_ops);
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 2 }
]
);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_insert_newline_at_beginning() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Insert { text: "\n".into() },
CharOperation::Keep { bytes: 9 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 2 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_delete_newline() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 4 },
CharOperation::Delete { bytes: 1 },
CharOperation::Keep { bytes: 4 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Delete { lines: 2 },
LineOperation::Insert { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_insert_multiple_newlines() {
let old_text = "aaaa\nbbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 5 },
CharOperation::Insert {
text: "\n\n".into(),
},
CharOperation::Keep { bytes: 4 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Insert { lines: 2 },
LineOperation::Keep { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_delete_multiple_newlines() {
let old_text = "aaaa\n\n\nbbbb";
let char_ops = vec![
CharOperation::Keep { bytes: 5 },
CharOperation::Delete { bytes: 2 },
CharOperation::Keep { bytes: 4 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 2 },
LineOperation::Keep { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_complex_scenario() {
let old_text = "line1\nline2\nline3\nline4";
let char_ops = vec![
CharOperation::Keep { bytes: 6 },
CharOperation::Insert {
text: "inserted\n".into(),
},
CharOperation::Delete { bytes: 6 },
CharOperation::Keep { bytes: 5 },
CharOperation::Insert {
text: "\nnewline".into(),
},
CharOperation::Keep { bytes: 6 },
];
let line_ops = char_ops_to_line_ops(&old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Keep { lines: 1 },
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 1 },
LineOperation::Insert { lines: 1 },
LineOperation::Keep { lines: 1 }
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(new_text, "line1\ninserted\nline3\nnewline\nline4");
assert_eq!(
apply_line_operations(old_text, &new_text, &line_ops),
new_text,
);
}
#[test]
fn test_cleaning_up_common_suffix() {
let old_text = concat!(
" for y in 0..size.y() {\n",
" let a = 10;\n",
" let b = 20;\n",
" }",
);
let char_ops = [
CharOperation::Keep { bytes: 8 },
CharOperation::Insert { text: "let".into() },
CharOperation::Insert {
text: " mut".into(),
},
CharOperation::Insert { text: " y".into() },
CharOperation::Insert { text: " =".into() },
CharOperation::Insert { text: " 0".into() },
CharOperation::Insert { text: ";".into() },
CharOperation::Insert { text: "\n".into() },
CharOperation::Insert {
text: " while".into(),
},
CharOperation::Insert { text: " y".into() },
CharOperation::Insert {
text: " < size".into(),
},
CharOperation::Insert { text: ".".into() },
CharOperation::Insert { text: "y".into() },
CharOperation::Insert { text: "()".into() },
CharOperation::Insert { text: " {".into() },
CharOperation::Insert { text: "\n".into() },
CharOperation::Delete { bytes: 23 },
CharOperation::Keep { bytes: 23 },
CharOperation::Keep { bytes: 1 },
CharOperation::Keep { bytes: 23 },
CharOperation::Keep { bytes: 1 },
CharOperation::Keep { bytes: 8 },
CharOperation::Insert {
text: " y".into(),
},
CharOperation::Insert { text: " +=".into() },
CharOperation::Insert { text: " 1".into() },
CharOperation::Insert { text: ";".into() },
CharOperation::Insert { text: "\n".into() },
CharOperation::Insert {
text: " ".into(),
},
CharOperation::Keep { bytes: 1 },
];
let line_ops = char_ops_to_line_ops(old_text, &char_ops);
assert_eq!(
line_ops,
vec![
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 2 },
LineOperation::Keep { lines: 2 },
LineOperation::Delete { lines: 1 },
LineOperation::Insert { lines: 2 },
]
);
let new_text = apply_char_operations(old_text, &char_ops);
assert_eq!(
new_text,
apply_line_operations(old_text, &new_text, &line_ops)
);
}
#[test]
fn test_random_diffs() {
random_test(|mut rng| {
let old_text_len = env::var("OLD_TEXT_LEN")
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
.unwrap_or(10);
let old = random_text(&mut rng, old_text_len);
println!("old text: {:?}", old);
let new = randomly_edit(&old, &mut rng);
println!("new text: {:?}", new);
let char_operations = random_streaming_diff(&mut rng, &old, &new);
println!("char operations: {:?}", char_operations);
// Use apply_char_operations to verify the result
let patched = apply_char_operations(&old, &char_operations);
assert_eq!(patched, new);
// Test char_ops_to_line_ops
let line_ops = char_ops_to_line_ops(&old, &char_operations);
println!("line operations: {:?}", line_ops);
let patched = apply_line_operations(&old, &new, &line_ops);
assert_eq!(patched, new);
});
}
fn char_ops_to_line_ops(old_text: &str, char_ops: &[CharOperation]) -> Vec<LineOperation> {
let old_rope = Rope::from(old_text);
let mut diff = LineDiff::default();
for op in char_ops {
diff.push_char_operation(op, &old_rope);
}
diff.finish(&old_rope);
diff.line_operations()
}
fn random_streaming_diff(rng: &mut impl Rng, old: &str, new: &str) -> Vec<CharOperation> {
let mut diff = StreamingDiff::new(old.to_string());
let mut char_operations = Vec::new();
let mut diff = StreamingDiff::new(old.clone());
let mut hunks = Vec::new();
let mut new_len = 0;
while new_len < new.len() {
let mut chunk_len = rng.gen_range(1..=new.len() - new_len);
while !new.is_char_boundary(new_len + chunk_len) {
chunk_len += 1;
}
let chunk = &new[new_len..new_len + chunk_len];
let new_hunks = diff.push_new(chunk);
char_operations.extend(new_hunks);
new_len += chunk_len;
let mut new = String::new();
while new_len < new_text_len {
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
let new_chunk = util::RandomCharIter::new(&mut rng)
.take(new_len)
.collect::<String>();
log::info!("new chunk: {:?}", new_chunk);
new_len += new_chunk_len;
new.push_str(&new_chunk);
let new_hunks = diff.push_new(&new_chunk);
log::info!("hunks: {:?}", new_hunks);
hunks.extend(new_hunks);
}
let final_hunks = diff.finish();
log::info!("final hunks: {:?}", final_hunks);
hunks.extend(final_hunks);
char_operations.extend(diff.finish());
char_operations
}
fn random_test<F>(mut test_fn: F)
where
F: FnMut(StdRng),
{
let iterations = env::var("ITERATIONS")
.map(|i| i.parse().expect("invalid `ITERATIONS` variable"))
.unwrap_or(100);
let seed: u64 = env::var("SEED")
.map(|s| s.parse().expect("invalid `SEED` variable"))
.unwrap_or(0);
println!(
"Running test with {} iterations and seed {}",
iterations, seed
);
for i in 0..iterations {
println!("Iteration {}", i + 1);
let rng = StdRng::seed_from_u64(seed + i);
test_fn(rng);
}
}
fn apply_line_operations(old_text: &str, new_text: &str, line_ops: &[LineOperation]) -> String {
let mut result: Vec<&str> = Vec::new();
let old_lines: Vec<&str> = old_text.split('\n').collect();
let new_lines: Vec<&str> = new_text.split('\n').collect();
let mut old_start = 0_usize;
let mut new_start = 0_usize;
for op in line_ops {
match op {
LineOperation::Keep { lines } => {
let old_end = old_start + *lines as usize;
result.extend(&old_lines[old_start..old_end]);
old_start = old_end;
new_start += *lines as usize;
}
LineOperation::Delete { lines } => {
old_start += *lines as usize;
}
LineOperation::Insert { lines } => {
let new_end = new_start + *lines as usize;
result.extend(&new_lines[new_start..new_end]);
new_start = new_end;
}
}
}
result.join("\n")
}
#[test]
fn test_apply_char_operations() {
let old_text = "Hello, world!";
let char_ops = vec![
CharOperation::Keep { bytes: 7 },
CharOperation::Delete { bytes: 5 },
CharOperation::Insert {
text: "Rust".to_string(),
},
CharOperation::Keep { bytes: 1 },
];
let result = apply_char_operations(old_text, &char_ops);
assert_eq!(result, "Hello, Rust!");
}
fn random_text(rng: &mut impl Rng, length: usize) -> String {
util::RandomCharIter::new(rng).take(length).collect()
}
fn randomly_edit(text: &str, rng: &mut impl Rng) -> String {
let mut result = String::from(text);
let edit_count = rng.gen_range(1..=5);
fn random_char_range(text: &str, rng: &mut impl Rng) -> (usize, usize) {
let mut start = rng.gen_range(0..=text.len());
while !text.is_char_boundary(start) {
start -= 1;
}
let mut end = rng.gen_range(start..=text.len());
while !text.is_char_boundary(end) {
end += 1;
}
(start, end)
}
for _ in 0..edit_count {
match rng.gen_range(0..3) {
0 => {
// Insert
let (pos, _) = random_char_range(&result, rng);
let insert_len = rng.gen_range(1..=5);
let insert_text: String = random_text(rng, insert_len);
result.insert_str(pos, &insert_text);
}
1 => {
// Delete
if !result.is_empty() {
let (start, end) = random_char_range(&result, rng);
result.replace_range(start..end, "");
}
}
2 => {
// Replace
if !result.is_empty() {
let (start, end) = random_char_range(&result, rng);
let replace_len = end - start;
let replace_text: String = random_text(rng, replace_len);
result.replace_range(start..end, &replace_text);
}
}
_ => unreachable!(),
}
}
result
}
fn apply_char_operations(old_text: &str, char_ops: &[CharOperation]) -> String {
let mut result = String::new();
log::info!("new text: {:?}", new);
let mut old_ix = 0;
for operation in char_ops {
match operation {
CharOperation::Keep { bytes } => {
result.push_str(&old_text[old_ix..old_ix + bytes]);
old_ix += bytes;
let mut new_ix = 0;
let mut patched = String::new();
for hunk in hunks {
match hunk {
Hunk::Keep { len } => {
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
patched.push_str(&old[old_ix..old_ix + len]);
old_ix += len;
new_ix += len;
}
CharOperation::Delete { bytes } => {
old_ix += bytes;
Hunk::Remove { len } => {
old_ix += len;
}
CharOperation::Insert { text } => {
result.push_str(text);
Hunk::Insert { text } => {
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
patched.push_str(&text);
new_ix += text.len();
}
}
}
result
assert_eq!(patched, new);
}
}

View File

@@ -0,0 +1,86 @@
When the user asks you to suggest edits for a buffer, use a strict template consisting of:
* A markdown code block with the file path as the language identifier.
* The original code that should be replaced
* A separator line (`---`)
* The new text that should replace the original lines
Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
## Example
If you have a buffer with the following lines:
```path/to/file.rs
fn quicksort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot_index = partition(arr);
let (left, right) = arr.split_at_mut(pivot_index);
quicksort(left);
quicksort(&mut right[1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
```
And you want to replace the for loop inside `partition`, output the following.
```edit path/to/file.rs
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
---
let mut j = 0;
while j < last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
j += 1;
}
```
If you wanted to insert comments above the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
---
// A helper function used for quicksort.
fn partition(arr: &mut [i32]) -> usize {
```
If you wanted to delete the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
---
```

View File

@@ -1,6 +1,6 @@
use crate::{
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
ModelSelector, DEFAULT_CONTEXT_LINES,
humanize_token_count, prompts::generate_terminal_assistant_prompt, AssistantPanel,
AssistantPanelEvent, LanguageModelCompletionProvider, ModelSelector,
};
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -16,9 +16,7 @@ use gpui::{
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
};
use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
use settings::Settings;
use std::{
cmp,
@@ -32,13 +30,8 @@ use ui::{prelude::*, IconButtonShape, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
cx: &mut AppContext,
) {
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
cx.set_global(TerminalInlineAssistant::new(fs, telemetry));
}
const PROMPT_HISTORY_MAX_LEN: usize = 20;
@@ -60,24 +53,18 @@ pub struct TerminalInlineAssistant {
prompt_history: VecDeque<String>,
telemetry: Option<Arc<Telemetry>>,
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
}
impl Global for TerminalInlineAssistant {}
impl TerminalInlineAssistant {
pub fn new(
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
) -> Self {
pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
Self {
next_assist_id: TerminalInlineAssistId::default(),
assists: HashMap::default(),
prompt_history: VecDeque::default(),
telemetry: Some(telemetry),
fs,
prompt_builder,
}
}
@@ -228,18 +215,17 @@ impl TerminalInlineAssistant {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok();
let (latest_output, working_directory) = assist
let working_directory = assist
.terminal
.update(cx, |terminal, cx| {
let terminal = terminal.model().read(cx);
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
let working_directory = terminal
terminal
.model()
.read(cx)
.working_directory()
.map(|path| path.to_string_lossy().to_string());
(latest_output, working_directory)
.map(|path| path.to_string_lossy().to_string())
})
.ok()
.unwrap_or_default();
.flatten();
let context_request = if assist.include_context {
assist.workspace.as_ref().and_then(|workspace| {
@@ -257,7 +243,7 @@ impl TerminalInlineAssistant {
None
};
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
let prompt = generate_terminal_assistant_prompt(
&assist
.prompt_editor
.clone()
@@ -266,8 +252,7 @@ impl TerminalInlineAssistant {
.prompt(cx),
shell.as_deref(),
working_directory.as_deref(),
&latest_output,
)?;
);
let mut messages = Vec::new();
if let Some(context_request) = context_request {
@@ -276,8 +261,7 @@ impl TerminalInlineAssistant {
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
content: prompt,
});
Ok(LanguageModelRequest {
@@ -564,7 +548,7 @@ impl Render for PromptEditor {
.gap_2()
.child(ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SlidersAlt)
IconButton::new("context", IconName::Settings)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
@@ -572,7 +556,7 @@ impl Render for PromptEditor {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
LanguageModelCompletionProvider::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
@@ -716,9 +700,6 @@ impl PromptEditor {
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
let assist_id = self.id;
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
self.pending_token_count = cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(1)).await;
let request =
@@ -726,7 +707,11 @@ impl PromptEditor {
inline_assistant.request_for_inline_assist(assist_id, cx)
})??;
let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
let token_count = cx
.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
})?
.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify();
@@ -855,7 +840,7 @@ impl PromptEditor {
}
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let model = LanguageModelRegistry::read_global(cx).active_model()?;
let model = LanguageModelCompletionProvider::read_global(cx).active_model()?;
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
@@ -921,7 +906,6 @@ impl PromptEditor {
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: rems(0.875).into(),
font_weight: settings.ui_font.weight,
line_height: relative(1.3),
@@ -959,7 +943,7 @@ impl TerminalTransaction {
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
// Ensure that the assistant cannot accidently execute commands that are streamed into the terminal
let input = hunk.replace(CARRIAGE_RETURN, " ");
self.terminal
.update(cx, |terminal, _| terminal.input(input));
@@ -997,16 +981,19 @@ impl Codegen {
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
let telemetry = self.telemetry.clone();
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
let telemetry = self.telemetry.clone();
let model_telemetry_id = LanguageModelCompletionProvider::read_global(cx)
.active_model()
.map(|m| m.telemetry_id())
.unwrap_or_default();
let response =
LanguageModelCompletionProvider::read_global(cx).stream_completion(prompt, cx);
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let response = model.stream_completion(prompt, &cx).await;
let response = response.await;
let generate = async {
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);

View File

@@ -1,803 +0,0 @@
mod step_view;
use crate::{
prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
};
use anyhow::{anyhow, Error, Result};
use collections::HashMap;
use editor::Editor;
use futures::future;
use gpui::{
Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView, WindowContext,
};
use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
use project::Project;
use rope::Point;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::{cmp, fmt::Write, ops::Range, sync::Arc};
use text::{AnchorRangeExt as _, OffsetRangeExt as _};
use util::ResultExt as _;
use workspace::Workspace;
pub use step_view::WorkflowStepView;
const IMPORTS_SYMBOL: &str = "#imports";
pub struct WorkflowStep {
context: WeakModel<Context>,
context_buffer_range: Range<Anchor>,
tool_output: String,
resolve_task: Option<Task<()>>,
pub resolution: Option<Result<WorkflowStepResolution, Arc<Error>>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowStepResolution {
pub title: String,
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkflowSuggestionGroup {
pub context_range: Range<language::Anchor>,
pub suggestions: Vec<WorkflowSuggestion>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkflowSuggestion {
Update {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
description: String,
},
CreateFile {
description: String,
},
InsertSiblingBefore {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
InsertSiblingAfter {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
PrependChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
AppendChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
Delete {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
},
}
impl WorkflowStep {
pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
Self {
context_buffer_range: range,
tool_output: String::new(),
context,
resolution: None,
resolve_task: None,
}
}
pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStep>) -> Option<()> {
let range = self.context_buffer_range.clone();
let context = self.context.upgrade()?;
let context = context.read(cx);
let project = context.project()?;
let prompt_builder = context.prompt_builder();
let mut request = context.to_completion_request(cx);
let model = LanguageModelRegistry::read_global(cx).active_model();
let context_buffer = context.buffer();
let step_text = context_buffer
.read(cx)
.text_for_range(range.clone())
.collect::<String>();
let mut workflow_context = String::new();
for message in context.messages(cx) {
write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
write!(&mut workflow_context, "{chunk}").unwrap();
}
write!(&mut workflow_context, "</message>").unwrap();
}
self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
let result = async {
let Some(model) = model else {
return Err(anyhow!("no model selected"));
};
this.update(&mut cx, |this, cx| {
this.tool_output.clear();
this.resolution = None;
this.result_updated(cx);
cx.notify();
})?;
let resolution_context = StepResolutionContext {
workflow_context,
step_to_resolve: step_text.clone(),
};
let mut prompt =
prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
prompt.push_str(&step_text);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
});
// Invoke the model to get its edit suggestions for this workflow step.
let mut stream = model
.use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
.await?;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
this.update(&mut cx, |this, cx| {
this.tool_output.push_str(&chunk);
cx.notify();
})?;
}
let resolution = this.update(&mut cx, |this, _| {
serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.tool_output)
})??;
this.update(&mut cx, |this, cx| {
this.tool_output = serde_json::to_string_pretty(&resolution).unwrap();
cx.notify();
})?;
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
let suggestion_tasks: Vec<_> = resolution
.suggestions
.iter()
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
.collect();
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
let suggestions = future::join_all(suggestion_tasks)
.await
.into_iter()
.filter_map(|task| task.log_err())
.collect::<Vec<_>>();
let mut suggestions_by_buffer = HashMap::default();
for (buffer, suggestion) in suggestions {
suggestions_by_buffer
.entry(buffer)
.or_insert_with(Vec::new)
.push(suggestion);
}
let mut suggestion_groups_by_buffer = HashMap::default();
for (buffer, mut suggestions) in suggestions_by_buffer {
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
// Sort suggestions by their range so that earlier, larger ranges come first
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
// Merge overlapping suggestions
suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
// Create context ranges for each suggestion
for suggestion in suggestions {
let context_range = {
let suggestion_point_range = suggestion.range().to_point(&snapshot);
let start_row = suggestion_point_range.start.row.saturating_sub(5);
let end_row = cmp::min(
suggestion_point_range.end.row + 5,
snapshot.max_point().row,
);
let start = snapshot.anchor_before(Point::new(start_row, 0));
let end = snapshot
.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
start..end
};
if let Some(last_group) = suggestion_groups.last_mut() {
if last_group
.context_range
.end
.cmp(&context_range.start, &snapshot)
.is_ge()
{
// Merge with the previous group if context ranges overlap
last_group.context_range.end = context_range.end;
last_group.suggestions.push(suggestion);
} else {
// Create a new group
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
} else {
// Create the first group
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
}
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
}
Ok((resolution.step_title, suggestion_groups_by_buffer))
};
let result = result.await;
this.update(&mut cx, |this, cx| {
this.resolution = Some(match result {
Ok((title, suggestion_groups)) => Ok(WorkflowStepResolution {
title,
suggestion_groups,
}),
Err(error) => Err(Arc::new(error)),
});
this.context
.update(cx, |context, cx| context.workflow_step_updated(range, cx))
.ok();
cx.notify();
})
.ok();
}));
None
}
fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
self.context
.update(cx, |context, cx| {
context.workflow_step_updated(self.context_buffer_range.clone(), cx)
})
.ok();
}
}
impl WorkflowSuggestion {
pub fn range(&self) -> Range<language::Anchor> {
match self {
Self::Update { range, .. } => range.clone(),
Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
Self::InsertSiblingBefore { position, .. }
| Self::InsertSiblingAfter { position, .. }
| Self::PrependChild { position, .. }
| Self::AppendChild { position, .. } => *position..*position,
Self::Delete { range, .. } => range.clone(),
}
}
pub fn description(&self) -> Option<&str> {
match self {
Self::Update { description, .. }
| Self::CreateFile { description }
| Self::InsertSiblingBefore { description, .. }
| Self::InsertSiblingAfter { description, .. }
| Self::PrependChild { description, .. }
| Self::AppendChild { description, .. } => Some(description),
Self::Delete { .. } => None,
}
}
fn description_mut(&mut self) -> Option<&mut String> {
match self {
Self::Update { description, .. }
| Self::CreateFile { description }
| Self::InsertSiblingBefore { description, .. }
| Self::InsertSiblingAfter { description, .. }
| Self::PrependChild { description, .. }
| Self::AppendChild { description, .. } => Some(description),
Self::Delete { .. } => None,
}
}
fn symbol_path(&self) -> Option<&SymbolPath> {
match self {
Self::Update { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
Self::Delete { symbol_path, .. } => Some(symbol_path),
Self::CreateFile { .. } => None,
}
}
fn kind(&self) -> &str {
match self {
Self::Update { .. } => "Update",
Self::CreateFile { .. } => "CreateFile",
Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
Self::PrependChild { .. } => "PrependChild",
Self::AppendChild { .. } => "AppendChild",
Self::Delete { .. } => "Delete",
}
}
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
let range = self.range();
let other_range = other.range();
// Don't merge if we don't contain the other suggestion.
if range.start.cmp(&other_range.start, buffer).is_gt()
|| range.end.cmp(&other_range.end, buffer).is_lt()
{
return false;
}
if let Some(description) = self.description_mut() {
if let Some(other_description) = other.description() {
description.push('\n');
description.push_str(other_description);
}
}
true
}
pub fn show(
&self,
editor: &View<Editor>,
excerpt_id: editor::ExcerptId,
workspace: &WeakView<Workspace>,
assistant_panel: &View<AssistantPanel>,
cx: &mut WindowContext,
) -> Option<InlineAssistId> {
let mut initial_transaction_id = None;
let initial_prompt;
let suggestion_range;
let buffer = editor.read(cx).buffer().clone();
let snapshot = buffer.read(cx).snapshot(cx);
match self {
Self::Update {
range, description, ..
} => {
initial_prompt = description.clone();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
}
Self::CreateFile { description } => {
initial_prompt = description.clone();
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
}
Self::InsertSiblingBefore {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::InsertSiblingAfter {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::PrependChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, false, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::AppendChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
suggestion_range = buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, false, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
Self::Delete { range, .. } => {
initial_prompt = "Delete".to_string();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
}
}
InlineAssistant::update_global(cx, |inline_assistant, cx| {
Some(inline_assistant.suggest_assist(
editor,
suggestion_range,
initial_prompt,
initial_transaction_id,
Some(workspace.clone()),
Some(assistant_panel),
cx,
))
})
}
}
pub mod tool {
use super::*;
use anyhow::Context as _;
use gpui::AsyncAppContext;
use language::{Outline, OutlineItem, ParseStatus};
use language_model::LanguageModelTool;
use project::ProjectPath;
use schemars::JsonSchema;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowStepResolutionTool {
/// An extremely short title for the edit step represented by these operations.
pub step_title: String,
/// A sequence of operations to apply to the codebase.
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
pub suggestions: Vec<WorkflowSuggestionTool>,
}
impl LanguageModelTool for WorkflowStepResolutionTool {
fn name() -> String {
"edit".into()
}
fn description() -> String {
"suggest edits to one or more locations in the codebase".into()
}
}
/// A description of an operation to apply to one location in the codebase.
///
/// This object represents a single edit operation that can be performed on a specific file
/// in the codebase. It encapsulates both the location (file path) and the nature of the
/// edit to be made.
///
/// # Fields
///
/// * `path`: A string representing the file path where the edit operation should be applied.
/// This path is relative to the root of the project or repository.
///
/// * `kind`: An enum representing the specific type of edit operation to be performed.
///
/// # Usage
///
/// `EditOperation` is used within a code editor to represent and apply
/// programmatic changes to source code. It provides a structured way to describe
/// edits for features like refactoring tools or AI-assisted coding suggestions.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowSuggestionTool {
/// The path to the file containing the relevant operation
pub path: String,
#[serde(flatten)]
pub kind: WorkflowSuggestionToolKind,
}
impl WorkflowSuggestionTool {
pub(super) async fn resolve(
&self,
project: Model<Project>,
mut cx: AsyncAppContext,
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
.update(&mut cx, |project, cx| {
let project_path = project
.find_project_path(Path::new(&path), cx)
.or_else(|| {
// If we couldn't find a project path for it, put it in the active worktree
// so that when we create the buffer, it can be saved.
let worktree = project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| project.worktrees(cx).next())?;
let worktree = worktree.read(cx);
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new(&path)),
})
})
.with_context(|| format!("worktree not found for {:?}", path))?;
anyhow::Ok(project.open_buffer(project_path, cx))
})??
.await?;
let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot.outline(None).context("no outline for buffer")?;
let suggestion = match kind {
WorkflowSuggestionToolKind::Update {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
let start = Point::new(start.row, 0);
let end = Point::new(
symbol.range.end.row,
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestion::Update {
range,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::Create { description } => {
WorkflowSuggestion::CreateFile { description }
}
WorkflowSuggestionToolKind::InsertSiblingBefore {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_before(
symbol
.annotation_range
.map_or(symbol.range.start, |annotation_range| {
annotation_range.start
}),
);
WorkflowSuggestion::InsertSiblingBefore {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::InsertSiblingAfter {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_after(symbol.range.end);
WorkflowSuggestion::InsertSiblingAfter {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::PrependChild {
symbol,
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_after(
symbol
.body_range
.map_or(symbol.range.start, |body_range| body_range.start),
);
WorkflowSuggestion::PrependChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestion::PrependChild {
position: language::Anchor::MIN,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::AppendChild {
symbol,
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let position = snapshot.anchor_before(
symbol
.body_range
.map_or(symbol.range.end, |body_range| body_range.end),
);
WorkflowSuggestion::AppendChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestion::PrependChild {
position: language::Anchor::MAX,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::Delete { symbol } => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
let start = Point::new(start.row, 0);
let end = Point::new(
symbol.range.end.row,
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestion::Delete { range, symbol_path }
}
};
Ok((buffer, suggestion))
}
fn resolve_symbol(
snapshot: &BufferSnapshot,
outline: &Outline<Anchor>,
symbol: &str,
) -> Result<(SymbolPath, OutlineItem<Point>)> {
if symbol == IMPORTS_SYMBOL {
let target_row = find_first_non_comment_line(snapshot);
Ok((
SymbolPath(IMPORTS_SYMBOL.to_string()),
OutlineItem {
range: Point::new(target_row, 0)..Point::new(target_row + 1, 0),
..Default::default()
},
))
} else {
let (symbol_path, symbol) = outline
.find_most_similar(symbol)
.with_context(|| format!("symbol not found: {symbol}"))?;
Ok((symbol_path, symbol.to_point(snapshot)))
}
}
}
fn find_first_non_comment_line(snapshot: &BufferSnapshot) -> u32 {
let Some(language) = snapshot.language() else {
return 0;
};
let scope = language.default_scope();
let comment_prefixes = scope.line_comment_prefixes();
let mut chunks = snapshot.as_rope().chunks();
let mut target_row = 0;
loop {
let starts_with_comment = chunks
.peek()
.map(|chunk| {
comment_prefixes
.iter()
.any(|s| chunk.starts_with(s.as_ref().trim_end()))
})
.unwrap_or(false);
if !starts_with_comment {
break;
}
target_row += 1;
if !chunks.next_line() {
break;
}
}
target_row
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
pub enum WorkflowSuggestionToolKind {
/// Rewrites the specified symbol entirely based on the given description.
/// This operation completely replaces the existing symbol with new content.
Update {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The path should uniquely identify the symbol within the containing file.
symbol: String,
/// A brief description of the transformation to apply to the symbol.
description: String,
},
/// Creates a new file with the given path based on the provided description.
/// This operation adds a new file to the codebase.
Create {
/// A brief description of the file to be created.
description: String,
},
/// Inserts a new symbol based on the given description before the specified symbol.
/// This operation adds new content immediately preceding an existing symbol.
InsertSiblingBefore {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The new content will be inserted immediately before this symbol.
symbol: String,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol based on the given description after the specified symbol.
/// This operation adds new content immediately following an existing symbol.
InsertSiblingAfter {
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// The new content will be inserted immediately after this symbol.
symbol: String,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol as a child of the specified symbol at the start.
/// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
PrependChild {
/// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// If provided, the new content will be inserted as the first child of this symbol.
/// If not provided, the new content will be inserted at the top of the file.
symbol: Option<String>,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Inserts a new symbol as a child of the specified symbol at the end.
/// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
AppendChild {
/// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
/// If provided, the new content will be inserted as the last child of this symbol.
/// If not provided, the new content will be applied at the bottom of the file.
symbol: Option<String>,
/// A brief description of the new symbol to be inserted.
description: String,
},
/// Deletes the specified symbol from the containing file.
Delete {
/// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
symbol: String,
},
}
}

View File

@@ -1,315 +0,0 @@
use super::WorkflowStep;
use crate::{Assist, Context};
use editor::{
display_map::{BlockDisposition, BlockProperties, BlockStyle},
Editor, EditorEvent, ExcerptRange, MultiBuffer,
};
use gpui::{
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
VisualContext as _, WeakModel, WindowContext,
};
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
use std::{ops::DerefMut, sync::Arc};
use text::OffsetRangeExt;
use theme::ActiveTheme as _;
use ui::{
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
InteractiveElement as _, Label, LabelCommon as _,
};
use workspace::{
item::{self, Item},
pane,
searchable::SearchableItemHandle,
};
pub struct WorkflowStepView {
step: WeakModel<WorkflowStep>,
tool_output_buffer: Model<Buffer>,
editor: View<Editor>,
}
impl WorkflowStepView {
pub fn new(
context: Model<Context>,
step: Model<WorkflowStep>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let tool_output_buffer =
cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
let buffer = cx.new_model(|cx| {
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
buffer.push_excerpts(
context.read(cx).buffer().clone(),
[ExcerptRange {
context: step.read(cx).context_buffer_range.clone(),
primary: None,
}],
cx,
);
buffer.push_excerpts(
tool_output_buffer.clone(),
[ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: None,
}],
cx,
);
buffer
});
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
let input_start_anchor = multi_buffer::Anchor::min();
let output_start_anchor = buffer_snapshot
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
.unwrap();
let output_end_anchor = multi_buffer::Anchor::max();
let handle = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_inline_completions(false);
editor.insert_blocks(
[
BlockProperties {
position: input_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Step Input", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Tool Output", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_end_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
if let Some(result) = handle.upgrade().and_then(|this| {
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
}) {
v_flex()
.child(section_header("Output", cx))
.child(
div().pl(cx.gutter_dimensions.full_width()).child(result),
)
.into_any_element()
} else {
Empty.into_any_element()
}
}),
disposition: BlockDisposition::Below,
priority: 0,
},
],
None,
cx,
);
editor
});
cx.observe(&step, Self::step_updated).detach();
cx.observe_release(&step, Self::step_released).detach();
cx.spawn(|this, mut cx| async move {
if let Ok(language) = language_registry.language_for_name("JSON").await {
this.update(&mut cx, |this, cx| {
this.tool_output_buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
})
.ok();
}
})
.detach();
Self {
tool_output_buffer,
step: step.downgrade(),
editor,
}
}
pub fn step(&self) -> &WeakModel<WorkflowStep> {
&self.step
}
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let step = self.step.upgrade()?;
let result = step.read(cx).resolution.as_ref()?;
match result {
Ok(result) => {
Some(
v_flex()
.child(result.title.clone())
.children(result.suggestion_groups.iter().filter_map(
|(buffer, suggestion_groups)| {
let buffer = buffer.read(cx);
let path = buffer.file().map(|f| f.path());
let snapshot = buffer.snapshot();
v_flex()
.mb_2()
.border_b_1()
.children(path.map(|path| format!("path: {}", path.display())))
.children(suggestion_groups.iter().map(|group| {
v_flex().pt_2().pl_2().children(
group.suggestions.iter().map(|suggestion| {
let range = suggestion.range().to_point(&snapshot);
v_flex()
.children(
suggestion.description().map(|desc| {
format!("description: {desc}")
}),
)
.child(format!("kind: {}", suggestion.kind()))
.children(suggestion.symbol_path().map(
|path| format!("symbol path: {}", path.0),
))
.child(format!(
"lines: {} - {}",
range.start.row + 1,
range.end.row + 1
))
}),
)
}))
.into()
},
))
.into_any_element(),
)
}
Err(error) => Some(format!("{:?}", error).into_any_element()),
}
}
fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
self.tool_output_buffer.update(cx, |buffer, cx| {
let text = step.read(cx).tool_output.clone();
buffer.set_text(text, cx);
});
cx.notify();
}
fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
cx.emit(EditorEvent::Closed);
}
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
self.step
.update(cx, |step, cx| {
step.resolve(cx);
})
.ok();
}
}
fn section_header(
name: &'static str,
cx: &mut editor::display_map::BlockContext,
) -> gpui::AnyElement {
h_flex()
.pl(cx.gutter_dimensions.full_width())
.h_11()
.w_full()
.relative()
.gap_1()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(Label::new(name).color(Color::Default)),
)
.into_any_element()
}
impl Render for WorkflowStepView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.key_context("ContextEditor")
.on_action(cx.listener(Self::resolve))
.flex_grow()
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for WorkflowStepView {}
impl FocusableView for WorkflowStepView {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
}
impl Item for WorkflowStepView {
type Event = EditorEvent;
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
let step = self.step.upgrade()?.read(cx);
let context = step.context.upgrade()?.read(cx);
let buffer = context.buffer().read(cx);
let index = context
.workflow_step_index_for_range(&step.context_buffer_range, buffer)
.ok()?
+ 1;
Some(format!("Step {index}").into())
}
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
Some(Icon::new(IconName::SearchCode))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event {
EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit);
}
EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab);
}
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
_ => {}
}
}
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
None
}
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, cx)
})
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::deactivated(editor, cx))
}
}

View File

@@ -15,45 +15,14 @@ pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AfterCompletion {
/// Run the command
Run,
/// Continue composing the current argument, doesn't add a space
Compose,
/// Continue the command composition, adds a space
Continue,
}
impl From<bool> for AfterCompletion {
fn from(value: bool) -> Self {
if value {
AfterCompletion::Run
} else {
AfterCompletion::Continue
}
}
}
impl AfterCompletion {
pub fn run(&self) -> bool {
match self {
AfterCompletion::Run => true,
AfterCompletion::Compose | AfterCompletion::Continue => false,
}
}
}
#[derive(Debug)]
pub struct ArgumentCompletion {
/// The label to display for this completion.
pub label: CodeLabel,
pub label: String,
/// The new text that should be inserted into the command when this completion is accepted.
pub new_text: String,
/// Whether the command should be run when accepting this completion.
pub after_completion: AfterCompletion,
/// Whether to replace the all arguments, or whether to treat this as an independent argument.
pub replace_previous_arguments: bool,
pub run_command: bool,
}
pub trait SlashCommand: 'static + Send + Sync {
@@ -65,25 +34,22 @@ pub trait SlashCommand: 'static + Send + Sync {
fn menu_text(&self) -> String;
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
query: String,
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
self.requires_argument()
}
fn run(
self: Arc<Self>,
arguments: &[String],
argument: Option<&str>,
workspace: WeakView<Workspace>,
// TODO: We're just using the `LspAdapterDelegate` here because that is
// what the extension API is already expecting.
//
// It may be that `LspAdapterDelegate` needs a more general name, or
// perhaps another kind of delegate is needed here.
delegate: Option<Arc<dyn LspAdapterDelegate>>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>>;
}
@@ -94,7 +60,7 @@ pub type RenderFoldPlaceholder = Arc<
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
>;
#[derive(Debug, Default)]
#[derive(Default)]
pub struct SlashCommandOutput {
pub text: String,
pub sections: Vec<SlashCommandOutputSection<usize>>,

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