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
243 changed files with 6414 additions and 4455 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

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

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ DerivedData/
.vscode
.wrangler
.flatpak-builder
.zed/debug.json

1113
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",
@@ -21,6 +21,8 @@ members = [
"crates/command_palette_hooks",
"crates/completion",
"crates/copilot",
"crates/dap",
"crates/debugger_ui",
"crates/db",
"crates/dev_server_projects",
"crates/diagnostics",
@@ -125,10 +127,6 @@ members = [
"crates/zed",
"crates/zed_actions",
#
# Extensions
#
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
@@ -158,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" }
@@ -192,7 +185,9 @@ command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
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" }
@@ -249,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" }
@@ -295,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 = "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/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" }
cargo_metadata = "0.18"
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"
@@ -346,13 +335,12 @@ 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"
@@ -374,6 +362,7 @@ 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"
@@ -403,7 +392,6 @@ smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
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"
@@ -427,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/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", 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"
@@ -448,19 +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"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.58"
version = "0.57"
features = [
"implement",
"Foundation_Numerics",
@@ -480,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",
@@ -498,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"
@@ -553,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 . .

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

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

@@ -40,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",
@@ -268,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 }],

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:
@@ -318,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.
@@ -681,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.
@@ -975,21 +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
"command_aliases": {}
}

View File

@@ -100,13 +100,21 @@ impl From<Role> for String {
#[derive(Debug, Serialize)]
pub struct Request {
pub model: String,
#[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,

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

@@ -314,7 +314,7 @@ impl AssistantPanel {
workspace.project().clone(),
Default::default(),
None,
NewFile.boxed_clone(),
Some(NewFile.boxed_clone()),
cx,
);
pane.set_can_split(false, cx);

View File

@@ -532,21 +532,7 @@ impl EditOperation {
.path_candidates
.iter()
.find(|item| item.string == symbol)
.with_context(|| {
format!(
"symbol {:?} not found in path {:?}.\ncandidates: {:?}.\nparse status: {:?}. text:\n{}",
symbol,
path,
outline
.path_candidates
.iter()
.map(|candidate| &candidate.string)
.collect::<Vec<_>>(),
*parse_status.borrow(),
buffer.read_with(&cx, |buffer, _| buffer.text()).unwrap_or_else(|_| "error".to_string())
)
})?;
.context("symbol not found")?;
buffer.update(&mut cx, |buffer, _| {
let outline_item = &outline.items[candidate.id];
let symbol_range = outline_item.range.to_point(buffer);
@@ -1137,17 +1123,16 @@ impl Context {
.timer(Duration::from_millis(200))
.await;
if let Some(token_count) = cx.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
})? {
let token_count = token_count.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify()
})?;
}
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()
})?;
anyhow::Ok(())
}
.log_err()

View File

@@ -1420,34 +1420,27 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(
ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SlidersAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelCompletionProvider::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
)
.with_info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",
),
)
.child(ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::Settings)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelCompletionProvider::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
@@ -1635,18 +1628,15 @@ impl PromptEditor {
})?
.await?;
if let Some(token_count) = cx.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
})? {
let token_count = token_count.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify();
})
} else {
Ok(())
}
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();
})
})
}
@@ -1835,7 +1825,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),

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
use fs::Fs;
use gpui::SharedString;
use language_model::LanguageModelRegistry;
use settings::update_settings_file;
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
@@ -12,7 +11,6 @@ pub struct ModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<ContextMenu>>,
fs: Arc<dyn Fs>,
trigger: T,
info_text: Option<SharedString>,
}
impl<T: PopoverTrigger> ModelSelector<T> {
@@ -21,7 +19,6 @@ impl<T: PopoverTrigger> ModelSelector<T> {
handle: None,
fs,
trigger,
info_text: None,
}
}
@@ -29,11 +26,6 @@ impl<T: PopoverTrigger> ModelSelector<T> {
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<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
@@ -43,20 +35,8 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
menu = menu.with_handle(handle);
}
let info_text = self.info_text.clone();
menu.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
if let Some(info_text) = info_text.clone() {
menu = menu
.custom_row(move |_cx| {
Label::new(info_text.clone())
.color(Color::Muted)
.into_any_element()
})
.separator();
}
for (index, provider) in LanguageModelRegistry::global(cx)
.read(cx)
.providers()

View File

@@ -734,29 +734,26 @@ impl PromptLibrary {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
if let Some(token_count) = cx.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::System,
content: body.to_string(),
}],
stop: Vec::new(),
temperature: 1.,
},
cx,
)
})? {
let token_count = token_count.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);
cx.notify();
})
} else {
Ok(())
}
let token_count = cx
.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::System,
content: body.to_string(),
}],
stop: Vec::new(),
temperature: 1.,
},
cx,
)
})?
.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);
cx.notify();
})
}
.log_err()
});

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()

View File

@@ -219,7 +219,7 @@ 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 items = store.search(package).await;

View File

@@ -29,7 +29,7 @@ impl FileSlashCommand {
let workspace = workspace.read(cx);
let project = workspace.project().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()

View File

@@ -707,18 +707,15 @@ impl PromptEditor {
inline_assistant.request_for_inline_assist(assist_id, cx)
})??;
if let Some(token_count) = cx.update(|cx| {
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
})? {
let token_count = token_count.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify();
})
} else {
Ok(())
}
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();
})
})
}
@@ -909,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),
@@ -947,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));

View File

@@ -0,0 +1,33 @@
[package]
name = "assistant_tooling"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_tooling.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true
project.workspace = true
repair_json.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
sum_tree.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,85 @@
# Assistant Tooling
Bringing Language Model tool calling to GPUI.
This unlocks:
- **Structured Extraction** of model responses
- **Validation** of model inputs
- **Execution** of chosen tools
## Overview
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When making a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
>
> **Assistant**: "Sure, I can help with that. Let me see what I can find."
>
> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]`
>
> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"`
>
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with two simple traits, `LanguageModelTool` and `ToolView`.
## Using the Tool Registry
```rust
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(WeatherTool { api_client },
})
.unwrap(); // You can only register one tool per name
let completion = cx.update(|cx| {
CompletionProvider::get(cx).complete(
model_name,
messages,
Vec::new(),
1.0,
// The definitions get passed directly to OpenAI when you want
// the model to be able to call your tool
tool_registry.definitions(),
)
});
let mut stream = completion?.await?;
let mut message = AssistantMessage::new();
while let Some(delta) = stream.next().await {
// As messages stream in, you'll get both assistant content
if let Some(content) = &delta.content {
message
.body
.update(cx, |message, cx| message.append(&content, cx));
}
// And tool calls!
for tool_call_delta in delta.tool_calls {
let index = tool_call_delta.index as usize;
if index >= message.tool_calls.len() {
message.tool_calls.resize_with(index + 1, Default::default);
}
let tool_call = &mut message.tool_calls[index];
// Build up an ID
if let Some(id) = &tool_call_delta.id {
tool_call.id.push_str(id);
}
tool_registry.update_tool_call(
tool_call,
tool_call_delta.name.as_deref(),
tool_call_delta.arguments.as_deref(),
cx,
);
}
}
```
Once the stream of tokens is complete, you can exexute the tool call by calling `tool_registry.execute_tool_call(tool_call, cx)`, which returns a `Task<Result<()>>`.
As the tokens stream in and tool calls are executed, your `ToolView` will get updates. Render each tool call by passing that `tool_call` in to `tool_registry.render_tool_call(tool_call, cx)`. The final message for the model can be pulled by calling `self.tool_registry.content_for_tool_call( tool_call, &mut project_context, cx, )`.

View File

@@ -0,0 +1,13 @@
mod attachment_registry;
mod project_context;
mod tool_registry;
pub use attachment_registry::{
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
UserAttachment,
};
pub use project_context::ProjectContext;
pub use tool_registry::{
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition,
ToolRegistry, ToolView,
};

View File

@@ -0,0 +1,234 @@
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use collections::HashMap;
use futures::future::join_all;
use gpui::{AnyView, Render, Task, View, WindowContext};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use util::ResultExt as _;
pub struct AttachmentRegistry {
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
}
pub trait AttachmentOutput {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
}
pub trait LanguageModelAttachment {
type Output: DeserializeOwned + Serialize + 'static;
type View: Render + AttachmentOutput;
fn name(&self) -> Arc<str>;
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
}
/// A collected attachment from running an attachment tool
pub struct UserAttachment {
pub view: AnyView,
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
}
#[derive(Serialize, Deserialize)]
pub struct SavedUserAttachment {
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
}
/// Internal representation of an attachment tool to allow us to treat them dynamically
struct RegisteredAttachment {
name: Arc<str>,
enabled: AtomicBool,
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
}
impl AttachmentRegistry {
pub fn new() -> Self {
Self {
registered_attachments: HashMap::default(),
}
}
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
let attachment = Arc::new(attachment);
let call = Box::new({
let attachment = attachment.clone();
move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = attachment.clone();
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let serialized_output =
result
.as_ref()
.map_err(ToString::to_string)
.and_then(|output| {
Ok(RawValue::from_string(
serde_json::to_string(output).map_err(|e| e.to_string())?,
)
.unwrap())
});
let view = cx.update(|cx| attachment.view(result, cx))?;
Ok(UserAttachment {
name: attachment.name(),
view: view.into(),
generate_fn: generate::<A>,
serialized_output,
})
})
}
});
let deserialize = Box::new({
let attachment = attachment.clone();
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
let serialized_output = saved_attachment.serialized_output.clone();
let output = match &serialized_output {
Ok(serialized_output) => {
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
}
Err(error) => Err(anyhow!("{error}")),
};
let view = attachment.view(output, cx).into();
Ok(UserAttachment {
name: saved_attachment.name.clone(),
view,
serialized_output,
generate_fn: generate::<A>,
})
}
});
self.registered_attachments.insert(
TypeId::of::<A>(),
RegisteredAttachment {
name: attachment.name(),
call,
deserialize,
enabled: AtomicBool::new(true),
},
);
return;
fn generate<T: LanguageModelAttachment>(
view: AnyView,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
view.downcast::<T::View>()
.unwrap()
.update(cx, |view, cx| T::View::generate(view, project, cx))
}
}
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
&self,
is_enabled: bool,
) {
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
attachment.enabled.store(is_enabled, SeqCst);
}
}
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
attachment.enabled.load(SeqCst)
} else {
false
}
}
pub fn call<A: LanguageModelAttachment + 'static>(
&self,
cx: &mut WindowContext,
) -> Task<Result<UserAttachment>> {
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
return Task::ready(Err(anyhow!("no attachment tool")));
};
(attachment.call)(cx)
}
pub fn call_all_attachment_tools(
self: Arc<Self>,
cx: &mut WindowContext<'_>,
) -> Task<Result<Vec<UserAttachment>>> {
let this = self.clone();
cx.spawn(|mut cx| async move {
let attachment_tasks = cx.update(|cx| {
let mut tasks = Vec::new();
for attachment in this
.registered_attachments
.values()
.filter(|attachment| attachment.enabled.load(SeqCst))
{
tasks.push((attachment.call)(cx))
}
tasks
})?;
let attachments = join_all(attachment_tasks.into_iter()).await;
Ok(attachments
.into_iter()
.filter_map(|attachment| attachment.log_err())
.collect())
})
}
pub fn serialize_user_attachment(
&self,
user_attachment: &UserAttachment,
) -> SavedUserAttachment {
SavedUserAttachment {
name: user_attachment.name.clone(),
serialized_output: user_attachment.serialized_output.clone(),
}
}
pub fn deserialize_user_attachment(
&self,
saved_user_attachment: SavedUserAttachment,
cx: &mut WindowContext,
) -> Result<UserAttachment> {
if let Some(registered_attachment) = self
.registered_attachments
.values()
.find(|attachment| attachment.name == saved_user_attachment.name)
{
(registered_attachment.deserialize)(&saved_user_attachment, cx)
} else {
Err(anyhow!(
"no attachment tool for name {}",
saved_user_attachment.name
))
}
}
}
impl UserAttachment {
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
let result = (self.generate_fn)(self.view.clone(), output, cx);
if result.is_empty() {
None
} else {
Some(result)
}
}
}

View File

@@ -0,0 +1,296 @@
use anyhow::{anyhow, Result};
use gpui::{AppContext, Model, Task, WeakModel};
use project::{Fs, Project, ProjectPath, Worktree};
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
use sum_tree::TreeMap;
pub struct ProjectContext {
files: TreeMap<ProjectPath, PathState>,
project: WeakModel<Project>,
fs: Arc<dyn Fs>,
}
#[derive(Debug, Clone)]
enum PathState {
PathOnly,
EntireFile,
Excerpts { ranges: Vec<Range<usize>> },
}
impl ProjectContext {
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
Self {
files: TreeMap::default(),
fs,
project,
}
}
pub fn add_path(&mut self, project_path: ProjectPath) {
if self.files.get(&project_path).is_none() {
self.files.insert(project_path, PathState::PathOnly);
}
}
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
let previous_state = self
.files
.get(&project_path)
.unwrap_or(&PathState::PathOnly);
let mut ranges = match previous_state {
PathState::EntireFile => return,
PathState::PathOnly => Vec::new(),
PathState::Excerpts { ranges } => ranges.to_vec(),
};
for new_range in new_ranges {
let ix = ranges.binary_search_by(|probe| {
if probe.end < new_range.start {
Ordering::Less
} else if probe.start > new_range.end {
Ordering::Greater
} else {
Ordering::Equal
}
});
match ix {
Ok(mut ix) => {
let existing = &mut ranges[ix];
existing.start = existing.start.min(new_range.start);
existing.end = existing.end.max(new_range.end);
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
ranges.remove(ix + 1);
}
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
ranges.remove(ix - 1);
ix -= 1;
}
}
Err(ix) => {
ranges.insert(ix, new_range.clone());
}
}
}
self.files
.insert(project_path, PathState::Excerpts { ranges });
}
pub fn add_file(&mut self, project_path: ProjectPath) {
self.files.insert(project_path, PathState::EntireFile);
}
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
let project = self
.project
.upgrade()
.ok_or_else(|| anyhow!("project dropped"));
let files = self.files.clone();
let fs = self.fs.clone();
cx.spawn(|cx| async move {
let project = project?;
let mut result = "project structure:\n".to_string();
let mut last_worktree: Option<Model<Worktree>> = None;
for (project_path, path_state) in files.iter() {
if let Some(worktree) = &last_worktree {
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
last_worktree = None;
}
}
let worktree;
if let Some(last_worktree) = &last_worktree {
worktree = last_worktree.clone();
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
project.worktree_for_id(project_path.worktree_id, cx)
})? {
worktree = tree;
last_worktree = Some(worktree.clone());
let worktree_name =
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
writeln!(&mut result, "# {}", worktree_name).unwrap();
} else {
continue;
}
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
let path = &project_path.path;
writeln!(&mut result, "## {}", path.display()).unwrap();
match path_state {
PathState::PathOnly => {}
PathState::EntireFile => {
let text = fs.load(&worktree_abs_path.join(&path)).await?;
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
}
PathState::Excerpts { ranges } => {
let text = fs.load(&worktree_abs_path.join(&path)).await?;
writeln!(&mut result, "~~~").unwrap();
// Assumption: ranges are in order, not overlapping
let mut prev_range_end = 0;
for range in ranges {
if range.start > prev_range_end {
writeln!(&mut result, "...").unwrap();
prev_range_end = range.end;
}
let mut start = range.start;
let mut end = range.end.min(text.len());
while !text.is_char_boundary(start) {
start += 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
result.push_str(&text[start..end]);
if !result.ends_with('\n') {
result.push('\n');
}
}
if prev_range_end < text.len() {
writeln!(&mut result, "...").unwrap();
}
writeln!(&mut result, "~~~").unwrap();
}
}
}
Ok(result)
})
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use gpui::TestAppContext;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use unindent::Unindent as _;
#[gpui::test]
async fn test_system_message_generation(cx: &mut TestAppContext) {
init_test(cx);
let file_3_contents = r#"
fn test1() {}
fn test2() {}
fn test3() {}
"#
.unindent();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/code",
json!({
"root1": {
"lib": {
"file1.rs": "mod example;",
"file2.rs": "",
},
"test": {
"file3.rs": file_3_contents,
}
},
"root2": {
"src": {
"main.rs": ""
}
}
}),
)
.await;
let project = Project::test(
fs.clone(),
["/code/root1".as_ref(), "/code/root2".as_ref()],
cx,
)
.await;
let worktree_ids = project.read_with(cx, |project, cx| {
project
.worktrees(cx)
.map(|worktree| worktree.read(cx).id())
.collect::<Vec<_>>()
});
let mut ax = ProjectContext::new(project.downgrade(), fs);
ax.add_file(ProjectPath {
worktree_id: worktree_ids[0],
path: Path::new("lib/file1.rs").into(),
});
let message = cx
.update(|cx| ax.generate_system_message(cx))
.await
.unwrap();
assert_eq!(
r#"
project structure:
# root1
## lib/file1.rs
~~~
mod example;
~~~
"#
.unindent(),
message
);
ax.add_excerpts(
ProjectPath {
worktree_id: worktree_ids[0],
path: Path::new("test/file3.rs").into(),
},
&[
file_3_contents.find("fn test2").unwrap()
..file_3_contents.find("fn test3").unwrap(),
],
);
let message = cx
.update(|cx| ax.generate_system_message(cx))
.await
.unwrap();
assert_eq!(
r#"
project structure:
# root1
## lib/file1.rs
~~~
mod example;
~~~
## test/file3.rs
~~~
...
fn test2() {}
...
~~~
"#
.unindent(),
message
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
});
}
}

View File

@@ -0,0 +1,526 @@
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
use repair_json::repair;
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
collections::HashMap,
fmt::Display,
mem,
sync::atomic::{AtomicBool, Ordering::SeqCst},
};
use ui::ViewContext;
pub struct ToolRegistry {
registered_tools: HashMap<String, RegisteredTool>,
}
#[derive(Default)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
state: ToolFunctionCallState,
}
#[derive(Default)]
enum ToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
KnownTool(Box<dyn InternalToolView>),
ExecutedTool(Box<dyn InternalToolView>),
}
trait InternalToolView {
fn view(&self) -> AnyView;
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
}
#[derive(Default, Serialize, Deserialize)]
pub struct SavedToolFunctionCall {
id: String,
name: String,
arguments: String,
state: SavedToolFunctionCallState,
}
#[derive(Default, Serialize, Deserialize)]
enum SavedToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
KnownTool,
ExecutedTool(Box<RawValue>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolFunctionDefinition {
pub name: String,
pub description: String,
pub parameters: RootSchema,
}
pub trait LanguageModelTool {
type View: ToolView;
/// Returns the name of the tool.
///
/// This name is exposed to the language model to allow the model to pick
/// which tools to use. As this name is used to identify the tool within a
/// tool registry, it should be unique.
fn name(&self) -> String;
/// Returns the description of the tool.
///
/// This can be used to _prompt_ the model as to what the tool does.
fn description(&self) -> String;
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
fn definition(&self) -> ToolFunctionDefinition {
let root_schema = schema_for!(<Self::View as ToolView>::Input);
ToolFunctionDefinition {
name: self.name(),
description: self.description(),
parameters: root_schema,
}
}
/// A view of the output of running the tool, for displaying to the user.
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
}
pub trait ToolView: Render {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: DeserializeOwned + JsonSchema;
/// The output returned by executing the tool.
type SerializedState: DeserializeOwned + Serialize;
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
fn deserialize(
&mut self,
output: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()>;
}
struct RegisteredTool {
enabled: AtomicBool,
type_id: TypeId,
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn InternalToolView>>,
definition: ToolFunctionDefinition,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
registered_tools: HashMap::new(),
}
}
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
for tool in self.registered_tools.values() {
if tool.type_id == TypeId::of::<T>() {
tool.enabled.store(is_enabled, SeqCst);
return;
}
}
}
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
for tool in self.registered_tools.values() {
if tool.type_id == TypeId::of::<T>() {
return tool.enabled.load(SeqCst);
}
}
false
}
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
self.registered_tools
.values()
.filter(|tool| tool.enabled.load(SeqCst))
.map(|tool| tool.definition.clone())
.collect()
}
pub fn update_tool_call(
&self,
call: &mut ToolFunctionCall,
name: Option<&str>,
arguments: Option<&str>,
cx: &mut WindowContext,
) {
if let Some(name) = name {
call.name.push_str(name);
}
if let Some(arguments) = arguments {
if call.arguments.is_empty() {
if let Some(tool) = self.registered_tools.get(&call.name) {
let view = (tool.build_view)(cx);
call.state = ToolFunctionCallState::KnownTool(view);
} else {
call.state = ToolFunctionCallState::NoSuchTool;
}
}
call.arguments.push_str(arguments);
if let ToolFunctionCallState::KnownTool(view) = &call.state {
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
view.try_set_input(&repaired_arguments, cx)
}
}
}
}
pub fn execute_tool_call(
&self,
tool_call: &mut ToolFunctionCall,
cx: &mut WindowContext,
) -> Option<Task<Result<()>>> {
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
let task = view.execute(cx);
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
Some(task)
} else {
None
}
}
pub fn render_tool_call(
&self,
tool_call: &ToolFunctionCall,
_cx: &mut WindowContext,
) -> Option<AnyElement> {
match &tool_call.state {
ToolFunctionCallState::NoSuchTool => {
Some(ui::Label::new("No such tool").into_any_element())
}
ToolFunctionCallState::Initializing => None,
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
Some(view.view().into_any_element())
}
}
}
pub fn content_for_tool_call(
&self,
tool_call: &ToolFunctionCall,
project_context: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match &tool_call.state {
ToolFunctionCallState::Initializing => String::new(),
ToolFunctionCallState::NoSuchTool => {
format!("No such tool: {}", tool_call.name)
}
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
view.generate(project_context, cx)
}
}
}
pub fn serialize_tool_call(
&self,
call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Result<SavedToolFunctionCall> {
Ok(SavedToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
ToolFunctionCallState::ExecutedTool(view) => {
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
}
},
})
}
pub fn deserialize_tool_call(
&self,
call: &SavedToolFunctionCall,
cx: &mut WindowContext,
) -> Result<ToolFunctionCall> {
let Some(tool) = self.registered_tools.get(&call.name) else {
return Err(anyhow!("no such tool {}", call.name));
};
Ok(ToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
SavedToolFunctionCallState::KnownTool => {
log::error!("Deserialized tool that had not executed");
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
ToolFunctionCallState::KnownTool(view)
}
SavedToolFunctionCallState::ExecutedTool(output) => {
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
view.deserialize_output(output, cx)?;
ToolFunctionCallState::ExecutedTool(view)
}
},
})
}
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
let name = tool.name();
let registered_tool = RegisteredTool {
type_id: TypeId::of::<T>(),
definition: tool.definition(),
enabled: AtomicBool::new(true),
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
};
let previous = self.registered_tools.insert(name.clone(), registered_tool);
if previous.is_some() {
return Err(anyhow!("already registered a tool with name {}", name));
}
return Ok(());
}
}
impl<T: ToolView> InternalToolView for View<T> {
fn view(&self) -> AnyView {
self.clone().into()
}
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
self.update(cx, |view, cx| view.generate(project, cx))
}
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
self.update(cx, |view, cx| {
view.set_input(input, cx);
cx.notify();
});
}
}
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |view, cx| view.execute(cx))
}
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
let output = self.update(cx, |view, cx| view.serialize(cx));
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
}
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
self.update(cx, |view, cx| view.deserialize(state, cx))?;
Ok(())
}
}
impl Display for ToolFunctionDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let schema = serde_json::to_string(&self.parameters).ok();
let schema = schema.unwrap_or("None".to_string());
write!(f, "Name: {}:\n", self.name)?;
write!(f, "Description: {}\n", self.description)?;
write!(f, "Parameters: {}", schema)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{div, prelude::*, Render, TestAppContext};
use gpui::{EmptyView, View};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize, Serialize, JsonSchema)]
struct WeatherQuery {
location: String,
unit: String,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
temperature: f64,
unit: String,
}
struct WeatherView {
input: Option<WeatherQuery>,
result: Option<WeatherResult>,
// Fake API call
current_weather: WeatherResult,
}
#[derive(Clone, Serialize)]
struct WeatherTool {
current_weather: WeatherResult,
}
impl WeatherView {
fn new(current_weather: WeatherResult) -> Self {
Self {
input: None,
result: None,
current_weather,
}
}
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
match self.result {
Some(ref result) => div()
.child(format!("temperature: {}", result.temperature))
.into_any_element(),
None => div().child("Calculating weather...").into_any_element(),
}
}
}
impl ToolView for WeatherView {
type Input = WeatherQuery;
type SerializedState = WeatherResult;
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
serde_json::to_string(&self.result).unwrap()
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
let input = self.input.as_ref().unwrap();
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
self.result = Some(weather);
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.current_weather.clone()
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
self.current_weather = output;
Ok(())
}
}
impl LanguageModelTool for WeatherTool {
type View = WeatherView;
fn name(&self) -> String {
"get_current_weather".to_string()
}
fn description(&self) -> String {
"Fetches the current weather for a given location.".to_string()
}
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
}
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
let mut registry = ToolRegistry::new();
registry
.register(WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
})
.unwrap();
let definitions = registry.definitions();
assert_eq!(
definitions,
[ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: serde_json::from_value(json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
}))
.unwrap(),
}]
);
let mut call = ToolFunctionCall {
id: "the-id".to_string(),
name: "get_cur".to_string(),
..Default::default()
};
let task = cx.update(|cx| {
registry.update_tool_call(
&mut call,
Some("rent_weather"),
Some(r#"{"location": "San Francisco","#),
cx,
);
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
registry.execute_tool_call(&mut call, cx).unwrap()
});
task.await.unwrap();
match &call.state {
ToolFunctionCallState::ExecutedTool(_view) => {}
_ => panic!(),
}
}
}

View File

@@ -493,7 +493,7 @@ impl Room {
// we leave the room and return an error.
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
@@ -942,7 +942,7 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
let _ = this.leave(cx).detach();
let _ = this.leave(cx);
}
this.user_store.update(cx, |user_store, cx| {

View File

@@ -7,9 +7,8 @@ pub mod user;
use anyhow::{anyhow, Context as _, Result};
use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{
client::IntoClientRequest,
error::Error as WebsocketError,
http::{HeaderValue, Request, StatusCode},
http::{Request, StatusCode},
};
use clock::SystemClock;
use collections::HashMap;
@@ -236,8 +235,6 @@ pub enum EstablishConnectionError {
#[error("{0}")]
Http(#[from] http_client::Error),
#[error("{0}")]
InvalidHeaderValue(#[from] async_tungstenite::tungstenite::http::header::InvalidHeaderValue),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Websocket(#[from] async_tungstenite::tungstenite::http::Error),
@@ -1162,24 +1159,19 @@ impl Client {
.ok()
.unwrap_or_default();
let request = Request::builder()
.header("Authorization", credentials.authorization_header())
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
.header("x-zed-app-version", app_version)
.header(
"x-zed-release-channel",
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
);
let http = self.http.clone();
let credentials = credentials.clone();
let rpc_url = self.rpc_url(http, release_channel);
cx.background_executor().spawn(async move {
use HttpOrHttps::*;
#[derive(Debug)]
enum HttpOrHttps {
Http,
Https,
}
let mut rpc_url = rpc_url.await?;
let url_scheme = match rpc_url.scheme() {
"https" => Https,
"http" => Http,
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
};
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1188,37 +1180,10 @@ impl Client {
log::info!("connected to rpc endpoint {}", rpc_url);
rpc_url
.set_scheme(match url_scheme {
Https => "wss",
Http => "ws",
})
.unwrap();
// We call `into_client_request` to let `tungstenite` construct the WebSocket request
// for us from the RPC URL.
//
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
let mut request = rpc_url.into_client_request()?;
// We then modify the request to add our desired headers.
let request_headers = request.headers_mut();
request_headers.insert(
"Authorization",
HeaderValue::from_str(&credentials.authorization_header())?,
);
request_headers.insert(
"x-zed-protocol-version",
HeaderValue::from_str(&rpc::PROTOCOL_VERSION.to_string())?,
);
request_headers.insert("x-zed-app-version", HeaderValue::from_str(&app_version)?);
request_headers.insert(
"x-zed-release-channel",
HeaderValue::from_str(&release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
);
match url_scheme {
Https => {
match rpc_url.scheme() {
"https" => {
rpc_url.set_scheme("wss").unwrap();
let request = request.uri(rpc_url.as_str()).body(())?;
let (stream, _) =
async_tungstenite::async_std::client_async_tls(request, stream).await?;
Ok(Connection::new(
@@ -1227,7 +1192,9 @@ impl Client {
.sink_map_err(|error| anyhow!(error)),
))
}
Http => {
"http" => {
rpc_url.set_scheme("ws").unwrap();
let request = request.uri(rpc_url.as_str()).body(())?;
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
Ok(Connection::new(
stream
@@ -1235,6 +1202,7 @@ impl Client {
.sink_map_err(|error| anyhow!(error)),
))
}
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
}
})
}

View File

@@ -18,7 +18,7 @@ use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
MemoryEvent, ReplEvent, SettingEvent,
MemoryEvent, SettingEvent,
};
use tempfile::NamedTempFile;
#[cfg(not(debug_assertions))]
@@ -531,21 +531,6 @@ impl Telemetry {
}
}
pub fn report_repl_event(
self: &Arc<Self>,
kernel_language: String,
kernel_status: String,
repl_session_id: String,
) {
let event = Event::Repl(ReplEvent {
kernel_language,
kernel_status,
repl_session_id,
});
self.report_event(event)
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();

View File

@@ -30,7 +30,7 @@ chrono.workspace = true
clock.workspace = true
clickhouse.workspace = true
collections.workspace = true
dashmap.workspace = true
dashmap = "5.4"
envy = "0.4.2"
futures.workspace = true
google_ai.workspace = true
@@ -47,7 +47,7 @@ prost.workspace = true
rand.workspace = true
reqwest = { version = "0.11", features = ["json"] }
rpc.workspace = true
scrypt = "0.11"
scrypt = "0.7"
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
semantic_version.workspace = true
semver.workspace = true

View File

@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use rpc::proto;
use util::ResultExt as _;
pub fn language_model_request_to_open_ai(
request: proto::CompleteWithLanguageModel,
@@ -20,7 +21,25 @@ pub fn language_model_request_to_open_ai(
proto::LanguageModelRole::LanguageModelAssistant => {
open_ai::RequestMessage::Assistant {
content: Some(message.content),
tool_calls: Vec::new(),
tool_calls: message
.tool_calls
.into_iter()
.filter_map(|call| {
Some(open_ai::ToolCall {
id: call.id,
content: match call.variant? {
proto::tool_call::Variant::Function(f) => {
open_ai::ToolCallContent::Function {
function: open_ai::FunctionContent {
name: f.name,
arguments: f.arguments,
},
}
}
},
})
})
.collect(),
}
}
proto::LanguageModelRole::LanguageModelSystem => {
@@ -28,6 +47,12 @@ pub fn language_model_request_to_open_ai(
content: message.content,
}
}
proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool {
tool_call_id: message
.tool_call_id
.ok_or_else(|| anyhow!("tool message is missing tool call id"))?,
content: message.content,
},
};
Ok(openai_message)
@@ -36,8 +61,32 @@ pub fn language_model_request_to_open_ai(
stream: true,
stop: request.stop,
temperature: request.temperature,
tool_choice: None,
tools: Vec::new(),
tools: request
.tools
.into_iter()
.filter_map(|tool| {
Some(match tool.variant? {
proto::chat_completion_tool::Variant::Function(f) => {
open_ai::ToolDefinition::Function {
function: open_ai::FunctionDefinition {
name: f.name,
description: f.description,
parameters: if let Some(params) = &f.parameters {
Some(
serde_json::from_str(params)
.context("failed to deserialize tool parameters")
.log_err()?,
)
} else {
None
},
},
}
}
})
})
.collect(),
tool_choice: request.tool_choice,
})
}
@@ -69,6 +118,9 @@ pub fn language_model_request_message_to_google_ai(
proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User,
proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model,
proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User,
proto::LanguageModelRole::LanguageModelTool => {
Err(anyhow!("we don't handle tool calls with google ai yet"))?
}
},
})
}

View File

@@ -1,4 +1,3 @@
pub mod contributors;
pub mod events;
pub mod extensions;
pub mod ips_file;
@@ -6,13 +5,13 @@ pub mod slack;
use crate::{
auth,
db::{User, UserId},
db::{ContributorSelector, User, UserId},
rpc, AppState, Error, Result,
};
use anyhow::anyhow;
use axum::{
body::Body,
extract::{Path, Query},
extract::{self, Path, Query},
http::{self, Request, StatusCode},
middleware::{self, Next},
response::IntoResponse,
@@ -20,6 +19,7 @@ use axum::{
Extension, Json, Router,
};
use axum_extra::response::ErasedJson;
use chrono::SecondsFormat;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower::ServiceBuilder;
@@ -31,7 +31,8 @@ pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Rou
.route("/user", get(get_authenticated_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.merge(contributors::router())
.route("/contributors", get(get_contributors).post(add_contributor))
.route("/contributor", get(check_is_contributor))
.layer(
ServiceBuilder::new()
.layer(Extension(state))
@@ -125,6 +126,66 @@ async fn get_rpc_server_snapshot(
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
}
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
Ok(Json(app.db.get_contributors().await?))
}
#[derive(Debug, Deserialize)]
struct CheckIsContributorParams {
github_user_id: Option<i32>,
github_login: Option<String>,
}
impl CheckIsContributorParams {
fn as_contributor_selector(self) -> Result<ContributorSelector> {
if let Some(github_user_id) = self.github_user_id {
return Ok(ContributorSelector::GitHubUserId { github_user_id });
}
if let Some(github_login) = self.github_login {
return Ok(ContributorSelector::GitHubLogin { github_login });
}
Err(anyhow!(
"must be one of `github_user_id` or `github_login`."
))?
}
}
#[derive(Debug, Serialize)]
struct CheckIsContributorResponse {
signed_at: Option<String>,
}
async fn check_is_contributor(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<CheckIsContributorParams>,
) -> Result<Json<CheckIsContributorResponse>> {
let params = params.as_contributor_selector()?;
Ok(Json(CheckIsContributorResponse {
signed_at: app
.db
.get_contributor_sign_timestamp(&params)
.await?
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
}))
}
async fn add_contributor(
Extension(app): Extension<Arc<AppState>>,
extract::Json(params): extract::Json<AuthenticatedUserParams>,
) -> Result<()> {
let initial_channel_id = app.config.auto_join_channel_id;
app.db
.add_contributor(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
initial_channel_id,
)
.await
}
#[derive(Deserialize)]
struct CreateAccessTokenQueryParams {
public_key: String,

View File

@@ -1,121 +0,0 @@
use std::sync::{Arc, OnceLock};
use anyhow::anyhow;
use axum::{
extract::{self, Query},
routing::get,
Extension, Json, Router,
};
use chrono::{NaiveDateTime, SecondsFormat};
use serde::{Deserialize, Serialize};
use crate::api::AuthenticatedUserParams;
use crate::db::ContributorSelector;
use crate::{AppState, Result};
pub fn router() -> Router {
Router::new()
.route("/contributors", get(get_contributors).post(add_contributor))
.route("/contributor", get(check_is_contributor))
}
async fn get_contributors(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<String>>> {
Ok(Json(app.db.get_contributors().await?))
}
#[derive(Debug, Deserialize)]
struct CheckIsContributorParams {
github_user_id: Option<i32>,
github_login: Option<String>,
}
impl CheckIsContributorParams {
fn as_contributor_selector(self) -> Result<ContributorSelector> {
if let Some(github_user_id) = self.github_user_id {
return Ok(ContributorSelector::GitHubUserId { github_user_id });
}
if let Some(github_login) = self.github_login {
return Ok(ContributorSelector::GitHubLogin { github_login });
}
Err(anyhow!(
"must be one of `github_user_id` or `github_login`."
))?
}
}
#[derive(Debug, Serialize)]
struct CheckIsContributorResponse {
signed_at: Option<String>,
}
async fn check_is_contributor(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<CheckIsContributorParams>,
) -> Result<Json<CheckIsContributorResponse>> {
let params = params.as_contributor_selector()?;
if RenovateBot::is_renovate_bot(&params) {
return Ok(Json(CheckIsContributorResponse {
signed_at: Some(
RenovateBot::created_at()
.and_utc()
.to_rfc3339_opts(SecondsFormat::Millis, true),
),
}));
}
Ok(Json(CheckIsContributorResponse {
signed_at: app
.db
.get_contributor_sign_timestamp(&params)
.await?
.map(|ts| ts.and_utc().to_rfc3339_opts(SecondsFormat::Millis, true)),
}))
}
/// The Renovate bot GitHub user (`renovate[bot]`).
///
/// https://api.github.com/users/renovate[bot]
struct RenovateBot;
impl RenovateBot {
const LOGIN: &'static str = "renovate[bot]";
const USER_ID: i32 = 29139614;
/// Returns the `created_at` timestamp for the Renovate bot user.
fn created_at() -> &'static NaiveDateTime {
static CREATED_AT: OnceLock<NaiveDateTime> = OnceLock::new();
CREATED_AT.get_or_init(|| {
chrono::DateTime::parse_from_rfc3339("2017-06-02T07:04:12Z")
.expect("failed to parse 'created_at' for 'renovate[bot]'")
.naive_utc()
})
}
/// Returns whether the given contributor selector corresponds to the Renovate bot user.
fn is_renovate_bot(contributor: &ContributorSelector) -> bool {
match contributor {
ContributorSelector::GitHubLogin { github_login } => github_login == Self::LOGIN,
ContributorSelector::GitHubUserId { github_user_id } => {
github_user_id == &Self::USER_ID
}
}
}
}
async fn add_contributor(
Extension(app): Extension<Arc<AppState>>,
extract::Json(params): extract::Json<AuthenticatedUserParams>,
) -> Result<()> {
let initial_channel_id = app.config.auto_join_channel_id;
app.db
.add_contributor(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
initial_channel_id,
)
.await
}

View File

@@ -16,7 +16,7 @@ use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent,
SettingEvent,
};
use uuid::Uuid;
@@ -518,13 +518,6 @@ pub async fn post_events(
checksum_matched,
))
}
Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
checksum_matched,
)),
}
}
@@ -549,7 +542,6 @@ struct ToUpload {
extension_events: Vec<ExtensionEventRow>,
edit_events: Vec<EditEventRow>,
action_events: Vec<ActionEventRow>,
repl_events: Vec<ReplEventRow>,
}
impl ToUpload {
@@ -625,11 +617,6 @@ impl ToUpload {
.await
.with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
const REPL_EVENTS_TABLE: &str = "repl_events";
Self::upload_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table '{REPL_EVENTS_TABLE}'"))?;
Ok(())
}
@@ -638,24 +625,22 @@ impl ToUpload {
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
if !rows.is_empty() {
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
}
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
Ok(())
}
}
@@ -1204,62 +1189,6 @@ impl ExtensionEventRow {
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct ReplEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
checksum_matched: bool,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// ReplEventRow
kernel_language: String,
kernel_status: String,
repl_session_id: String,
}
impl ReplEventRow {
fn from_event(
event: ReplEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
checksum_matched: bool,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|v| v.major() as i32),
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
checksum_matched,
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
kernel_language: event.kernel_language,
kernel_status: event.kernel_status,
repl_session_id: event.repl_session_id,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditEventRow {
// AppInfoBase

View File

@@ -9,7 +9,6 @@ use axum::{
middleware::Next,
response::IntoResponse,
};
use base64::prelude::*;
use prometheus::{exponential_buckets, register_histogram, Histogram};
pub use rpc::auth::random_token;
use scrypt::{
@@ -156,7 +155,10 @@ pub async fn create_access_token(
/// protection.
pub fn hash_access_token(token: &str) -> String {
let digest = sha2::Sha256::digest(token);
format!("$sha256${}", BASE64_URL_SAFE.encode(digest))
format!(
"$sha256${}",
base64::encode_config(digest, base64::URL_SAFE)
)
}
/// Encrypts the given access token with the given public key to avoid leaking it on the way
@@ -400,16 +402,15 @@ mod test {
fn previous_hash_access_token(token: &str) -> Result<String> {
// Avoid slow hashing in debug mode.
let params = if cfg!(debug_assertions) {
scrypt::Params::new(1, 1, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
scrypt::Params::new(1, 1, 1).unwrap()
} else {
scrypt::Params::new(14, 8, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
scrypt::Params::new(14, 8, 1).unwrap()
};
Ok(Scrypt
.hash_password_customized(
.hash_password(
token.as_bytes(),
None,
None,
params,
&SaltString::generate(thread_rng()),
)

View File

@@ -153,7 +153,7 @@ async fn main() -> Result<()> {
let signal = async move {
// todo(windows):
// `ctrl_close` does not work well, because tokio's signal handler always returns soon,
// but system terminates the application soon after returning CTRL+CLOSE handler.
// but system termiates the application soon after returning CTRL+CLOSE handler.
// So we should implement blocking handler to treat CTRL+CLOSE signal.
let mut ctrl_break = tokio::signal::windows::ctrl_break()
.expect("failed to listen for interrupt signal");

View File

@@ -12,7 +12,7 @@ use crate::{
executor::Executor,
AppState, Error, RateLimit, RateLimiter, Result,
};
use anyhow::{anyhow, bail, Context as _};
use anyhow::{anyhow, Context as _};
use async_tungstenite::tungstenite::{
protocol::CloseFrame as TungsteniteCloseFrame, Message as TungsteniteMessage,
};
@@ -1392,7 +1392,7 @@ pub async fn handle_websocket_request(
let socket = socket
.map_ok(to_tungstenite_message)
.err_into()
.with(|message| async move { to_axum_message(message) });
.with(|message| async move { Ok(to_axum_message(message)) });
let connection = Connection::new(Box::pin(socket));
async move {
server
@@ -4597,19 +4597,37 @@ async fn complete_with_open_ai(
.map(|choice| proto::LanguageModelChoiceDelta {
index: choice.index,
delta: Some(proto::LanguageModelResponseMessage {
role: choice.delta.role.and_then(|role| match role {
open_ai::Role::User => {
Some(LanguageModelRole::LanguageModelUser as i32)
}
open_ai::Role::Assistant => {
Some(LanguageModelRole::LanguageModelAssistant as i32)
}
open_ai::Role::System => {
Some(LanguageModelRole::LanguageModelSystem as i32)
}
_ => None,
}),
role: choice.delta.role.map(|role| match role {
open_ai::Role::User => LanguageModelRole::LanguageModelUser,
open_ai::Role::Assistant => LanguageModelRole::LanguageModelAssistant,
open_ai::Role::System => LanguageModelRole::LanguageModelSystem,
open_ai::Role::Tool => LanguageModelRole::LanguageModelTool,
} as i32),
content: choice.delta.content,
tool_calls: choice
.delta
.tool_calls
.unwrap_or_default()
.into_iter()
.map(|delta| proto::ToolCallDelta {
index: delta.index as u32,
id: delta.id,
variant: match delta.function {
Some(function) => {
let name = function.name;
let arguments = function.arguments;
Some(proto::tool_call_delta::Variant::Function(
proto::tool_call_delta::FunctionCallDelta {
name,
arguments,
},
))
}
None => None,
},
})
.collect(),
}),
finish_reason: choice.finish_reason,
})
@@ -4661,6 +4679,8 @@ async fn complete_with_google_ai(
})
.collect(),
),
// Tool calls are not supported for Google
tool_calls: Vec::new(),
}),
finish_reason: candidate.finish_reason.map(|reason| reason.to_string()),
})
@@ -4677,6 +4697,8 @@ async fn complete_with_anthropic(
session: UserSession,
api_key: Arc<str>,
) -> Result<()> {
let model = anthropic::Model::from_id(&request.model)?;
let mut system_message = String::new();
let messages = request
.messages
@@ -4701,6 +4723,8 @@ async fn complete_with_anthropic(
None
}
// We don't yet support tool calls for Anthropic
LanguageModelRole::LanguageModelTool => None,
}
})
.collect();
@@ -4710,7 +4734,7 @@ async fn complete_with_anthropic(
anthropic::ANTHROPIC_API_URL,
&api_key,
anthropic::Request {
model: request.model,
model,
messages,
stream: true,
system: system_message,
@@ -4745,6 +4769,7 @@ async fn complete_with_anthropic(
delta: Some(proto::LanguageModelResponseMessage {
role: Some(current_role as i32),
content: Some(text),
tool_calls: Vec::new(),
}),
finish_reason: None,
}],
@@ -4761,6 +4786,7 @@ async fn complete_with_anthropic(
delta: Some(proto::LanguageModelResponseMessage {
role: Some(current_role as i32),
content: Some(text),
tool_calls: Vec::new(),
}),
finish_reason: None,
}],
@@ -5128,8 +5154,8 @@ async fn get_private_user_info(
Ok(())
}
fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
let message = match message {
fn to_axum_message(message: TungsteniteMessage) -> AxumMessage {
match message {
TungsteniteMessage::Text(payload) => AxumMessage::Text(payload),
TungsteniteMessage::Binary(payload) => AxumMessage::Binary(payload),
TungsteniteMessage::Ping(payload) => AxumMessage::Ping(payload),
@@ -5138,20 +5164,7 @@ fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
code: frame.code.into(),
reason: frame.reason,
})),
// We should never receive a frame while reading the message, according
// to the `tungstenite` maintainers:
//
// > It cannot occur when you read messages from the WebSocket, but it
// > can be used when you want to send the raw frames (e.g. you want to
// > send the frames to the WebSocket without composing the full message first).
// >
// > — https://github.com/snapview/tungstenite-rs/issues/268
TungsteniteMessage::Frame(_) => {
bail!("received an unexpected frame while reading the message")
}
};
Ok(message)
}
}
fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {

View File

@@ -1,4 +1,3 @@
#![allow(clippy::reversed_empty_ranges)]
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use call::{ActiveCall, ParticipantLocation};
use client::ChannelId;

View File

@@ -533,7 +533,6 @@ impl Render for MessageEditor {
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,

View File

@@ -2190,7 +2190,6 @@ impl CollabPanel {
},
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,
font_style: FontStyle::Normal,

View File

@@ -1,5 +1,5 @@
use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AppContext, Global, Model, ModelContext, Task};
use language_model::{
LanguageModel, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
@@ -27,7 +27,7 @@ pub struct LanguageModelCompletionProvider {
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
pub struct LanguageModelCompletionResponse {
inner: BoxStream<'static, Result<String>>,
pub inner: BoxStream<'static, Result<String>>,
_lock: SemaphoreGuardArc,
}
@@ -143,11 +143,11 @@ impl LanguageModelCompletionProvider {
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> Option<BoxFuture<'static, Result<usize>>> {
) -> BoxFuture<'static, Result<usize>> {
if let Some(model) = self.active_model() {
Some(model.count_tokens(request, cx))
model.count_tokens(request, cx)
} else {
None
std::future::ready(Err(anyhow!("No active model set"))).boxed()
}
}

View File

@@ -691,7 +691,7 @@ impl Copilot {
{
match event {
language::Event::Edited => {
drop(registered_buffer.report_changes(&buffer, cx));
let _ = registered_buffer.report_changes(&buffer, cx);
}
language::Event::Saved => {
server

View File

@@ -333,7 +333,7 @@ mod tests {
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
@@ -341,7 +341,7 @@ mod tests {
three
"},
vec!["completion_a", "completion_b"],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -375,7 +375,7 @@ mod tests {
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
@@ -383,7 +383,7 @@ mod tests {
three
"},
vec![],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -408,7 +408,7 @@ mod tests {
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
@@ -416,7 +416,7 @@ mod tests {
three
"},
vec!["completion_a", "completion_b"],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -590,7 +590,7 @@ mod tests {
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
@@ -598,7 +598,7 @@ mod tests {
three
"},
vec![],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -632,7 +632,7 @@ mod tests {
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
@@ -640,7 +640,7 @@ mod tests {
three
"},
vec![],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -889,7 +889,7 @@ mod tests {
three
"});
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
@@ -897,7 +897,7 @@ mod tests {
three
"},
vec!["completion_a", "completion_b"],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -917,7 +917,7 @@ mod tests {
});
cx.simulate_keystroke("o");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
@@ -925,7 +925,7 @@ mod tests {
three
"},
vec!["completion_a_2", "completion_b_2"],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
@@ -944,7 +944,7 @@ mod tests {
});
cx.simulate_keystroke(".");
drop(handle_completion_request(
let _ = handle_completion_request(
&mut cx,
indoc! {"
one
@@ -952,7 +952,7 @@ mod tests {
three
"},
vec!["something_else()"],
));
);
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {

27
crates/dap/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "dap"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
async-std = "1.12.0"
dap-types = { git = "https://github.com/zed-industries/dap-types" }
futures.workspace = true
gpui.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
smol.workspace = true
task.workspace = true
util.workspace = true

646
crates/dap/src/client.rs Normal file
View File

@@ -0,0 +1,646 @@
use crate::transport::{Events, Payload, Response, Transport};
use anyhow::{anyhow, Context, Result};
use dap_types::{
requests::{
Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request,
Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut,
},
AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse,
DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments,
NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments,
RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source,
SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments,
StepInArguments, StepOutArguments, SteppingGranularity, Variable,
};
use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite};
use gpui::{AppContext, AsyncAppContext};
use parking_lot::{Mutex, MutexGuard};
use serde_json::Value;
use smol::{
channel::{bounded, unbounded, Receiver, Sender},
io::BufReader,
net::{TcpListener, TcpStream},
process::{self, Child},
};
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddrV4},
path::PathBuf,
process::Stdio,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost};
use util::ResultExt;
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ThreadStatus {
#[default]
Running,
Stopped,
Exited,
Ended,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct DebugAdapterClientId(pub usize);
#[derive(Debug, Default, Clone)]
pub struct ThreadState {
pub status: ThreadStatus,
pub stack_frames: Vec<StackFrame>,
pub scopes: HashMap<u64, Vec<Scope>>, // stack_frame_id -> scopes
pub variables: HashMap<u64, Vec<Variable>>, // scope.variable_reference -> variables
pub current_stack_frame_id: Option<u64>,
}
pub struct DebugAdapterClient {
id: DebugAdapterClientId,
_process: Option<Child>,
server_tx: Sender<Payload>,
sequence_count: AtomicU64,
capabilities: Arc<Mutex<Option<dap_types::Capabilities>>>,
config: DebugAdapterConfig,
thread_states: Arc<Mutex<HashMap<u64, ThreadState>>>, // thread_id -> thread_state
sub_client: Arc<Mutex<Option<Arc<Self>>>>,
}
impl DebugAdapterClient {
/// Creates & returns a new debug adapter client
///
/// # Parameters
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
/// - `config`: The adapter specific configurations from debugger task that is starting
/// - `command`: The command that starts the debugger
/// - `args`: Arguments of the command that starts the debugger
/// - `project_path`: The absolute path of the project that is being debugged
/// - `cx`: The context that the new client belongs too
pub async fn new<F>(
id: DebugAdapterClientId,
config: DebugAdapterConfig,
command: &str,
args: Vec<&str>,
project_path: PathBuf,
event_handler: F,
cx: &mut AsyncAppContext,
) -> Result<Arc<Self>>
where
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
{
match config.connection.clone() {
DebugConnectionType::TCP(host) => {
Self::create_tcp_client(
id,
config,
host,
command,
args,
project_path,
event_handler,
cx,
)
.await
}
DebugConnectionType::STDIO => {
Self::create_stdio_client(
id,
config,
command,
args,
project_path,
event_handler,
cx,
)
.await
}
}
}
/// Creates a debug client that connects to an adapter through tcp
///
/// TCP clients don't have an error communication stream with an adapter
///
/// # Parameters
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
/// - `config`: The adapter specific configurations from debugger task that is starting
/// - `command`: The command that starts the debugger
/// - `args`: Arguments of the command that starts the debugger
/// - `project_path`: The absolute path of the project that is being debugged
/// - `cx`: The context that the new client belongs too
#[allow(clippy::too_many_arguments)]
async fn create_tcp_client<F>(
id: DebugAdapterClientId,
config: DebugAdapterConfig,
host: TCPHost,
command: &str,
args: Vec<&str>,
project_path: PathBuf,
event_handler: F,
cx: &mut AsyncAppContext,
) -> Result<Arc<Self>>
where
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
{
let mut port = host.port;
if port.is_none() {
port = Self::get_port().await;
}
let mut command = process::Command::new(command);
command
.current_dir(project_path)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
let process = command
.spawn()
.with_context(|| "failed to start debug adapter.")?;
if let Some(delay) = host.delay {
// some debug adapters need some time to start the TCP server
// so we have to wait few milliseconds before we can connect to it
cx.background_executor()
.timer(Duration::from_millis(delay))
.await;
}
let address = SocketAddrV4::new(
host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)),
port.unwrap(),
);
let (rx, tx) = TcpStream::connect(address).await?.split();
Self::handle_transport(
id,
config,
Box::new(BufReader::new(rx)),
Box::new(tx),
None,
Some(process),
event_handler,
cx,
)
}
/// Get an open port to use with the tcp client when not supplied by debug config
async fn get_port() -> Option<u16> {
Some(
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0))
.await
.ok()?
.local_addr()
.ok()?
.port(),
)
}
/// Creates a debug client that connects to an adapter through std input/output
///
/// # Parameters
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
/// - `config`: The adapter specific configurations from debugger task that is starting
/// - `command`: The command that starts the debugger
/// - `args`: Arguments of the command that starts the debugger
/// - `project_path`: The absolute path of the project that is being debugged
/// - `cx`: The context that the new client belongs too
async fn create_stdio_client<F>(
id: DebugAdapterClientId,
config: DebugAdapterConfig,
command: &str,
args: Vec<&str>,
project_path: PathBuf,
event_handler: F,
cx: &mut AsyncAppContext,
) -> Result<Arc<Self>>
where
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
{
let mut command = process::Command::new(command);
command
.current_dir(project_path)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command
.spawn()
.with_context(|| "failed to spawn command.")?;
let stdin = process
.stdin
.take()
.ok_or_else(|| anyhow!("Failed to open stdin"))?;
let stdout = process
.stdout
.take()
.ok_or_else(|| anyhow!("Failed to open stdout"))?;
let stderr = process
.stderr
.take()
.ok_or_else(|| anyhow!("Failed to open stderr"))?;
let stdin = Box::new(stdin);
let stdout = Box::new(BufReader::new(stdout));
let stderr = Box::new(BufReader::new(stderr));
Self::handle_transport(
id,
config,
stdout,
stdin,
Some(stderr),
Some(process),
event_handler,
cx,
)
}
#[allow(clippy::too_many_arguments)]
pub fn handle_transport<F>(
id: DebugAdapterClientId,
config: DebugAdapterConfig,
rx: Box<dyn AsyncBufRead + Unpin + Send>,
tx: Box<dyn AsyncWrite + Unpin + Send>,
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
process: Option<Child>,
event_handler: F,
cx: &mut AsyncAppContext,
) -> Result<Arc<Self>>
where
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
{
let (server_rx, server_tx) = Transport::start(rx, tx, err, cx);
let (client_tx, client_rx) = unbounded::<Payload>();
let client = Arc::new(Self {
id,
config,
_process: process,
sub_client: Default::default(),
server_tx: server_tx.clone(),
capabilities: Default::default(),
thread_states: Default::default(),
sequence_count: AtomicU64::new(1),
});
cx.update(|cx| {
cx.background_executor()
.spawn(Self::handle_recv(server_rx, client_tx))
.detach_and_log_err(cx);
cx.spawn({
let client = client.clone();
|mut cx| async move {
Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await
}
})
.detach_and_log_err(cx);
})?;
Ok(client)
}
/// Set's up a client's event handler.
///
/// This function should only be called once or else errors will arise
/// # Parameters
/// `client`: A pointer to the client to pass the event handler too
/// `event_handler`: The function that is called to handle events
/// should be DebugPanel::handle_debug_client_events
/// `cx`: The context that this task will run in
pub async fn handle_events<F>(
this: Arc<Self>,
client_rx: Receiver<Payload>,
server_tx: Sender<Payload>,
mut event_handler: F,
cx: &mut AsyncAppContext,
) -> Result<()>
where
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
{
while let Ok(payload) = client_rx.recv().await {
match payload {
Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?,
Payload::Request(request) => {
if RunInTerminal::COMMAND == request.command {
Self::handle_run_in_terminal_request(request, &server_tx).await?;
} else if StartDebugging::COMMAND == request.command {
Self::handle_start_debugging_request(&this, request, cx).await?;
} else {
unreachable!("Unknown reverse request {}", request.command);
}
}
_ => unreachable!(),
}
}
anyhow::Ok(())
}
async fn handle_run_in_terminal_request(
request: crate::transport::Request,
server_tx: &Sender<Payload>,
) -> Result<()> {
let arguments: RunInTerminalRequestArguments =
serde_json::from_value(request.arguments.unwrap_or_default())?;
let mut args = arguments.args.clone();
let mut command = process::Command::new(args.remove(0));
let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| {
e.iter()
.map(|(key, value)| (key.clone(), value.clone().to_string()))
.collect::<HashMap<String, String>>()
});
if let Some(envs) = envs {
command.envs(envs);
}
let process = command
.current_dir(arguments.cwd)
.args(args)
.spawn()
.with_context(|| "failed to spawn run in terminal command.")?;
server_tx
.send(Payload::Response(Response {
request_seq: request.seq,
success: true,
command: RunInTerminal::COMMAND.into(),
message: None,
body: Some(serde_json::to_value(RunInTerminalResponse {
process_id: Some(process.id() as u64),
shell_process_id: None,
})?),
}))
.await?;
anyhow::Ok(())
}
async fn handle_start_debugging_request(
this: &Arc<Self>,
request: crate::transport::Request,
cx: &mut AsyncAppContext,
) -> Result<()> {
dbg!(&request);
let arguments: StartDebuggingRequestArguments =
serde_json::from_value(request.arguments.clone().unwrap_or_default())?;
let sub_client = DebugAdapterClient::new(
DebugAdapterClientId(1),
this.config.clone(),
"node",
vec![
"/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js",
"8134",
"127.0.0.1",
],
PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"),
|event, _cx| {
dbg!(event);
},
cx,
)
.await?;
dbg!(&arguments);
let res = sub_client.launch(request.arguments).await?;
dbg!(res);
*this.sub_client.lock() = Some(sub_client);
anyhow::Ok(())
}
async fn handle_recv(server_rx: Receiver<Payload>, client_tx: Sender<Payload>) -> Result<()> {
while let Ok(payload) = server_rx.recv().await {
match payload {
Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?,
Payload::Response(_) => unreachable!(),
Payload::Request(req) => client_tx.send(Payload::Request(req)).await?,
};
}
anyhow::Ok(())
}
/// Send a request to an adapter and get a response back
/// Note: This function will block until a response is sent back from the adapter
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
let serialized_arguments = serde_json::to_value(arguments)?;
let (callback_tx, callback_rx) = bounded::<Result<Response>>(1);
let request = crate::transport::Request {
back_ch: Some(callback_tx),
seq: self.next_sequence_id(),
command: R::COMMAND.to_string(),
arguments: Some(serialized_arguments),
};
self.server_tx.send(Payload::Request(request)).await?;
let response = callback_rx.recv().await??;
match response.success {
true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
false => Err(anyhow!("Request failed")),
}
}
pub fn id(&self) -> DebugAdapterClientId {
self.id
}
pub fn config(&self) -> DebugAdapterConfig {
self.config.clone()
}
pub fn request_type(&self) -> DebugRequestType {
self.config.request.clone()
}
pub fn capabilities(&self) -> dap_types::Capabilities {
self.capabilities.lock().clone().unwrap_or_default()
}
/// Get the next sequence id to be used in a request
/// # Side Effect
/// This function also increment's client's sequence count by one
pub fn next_sequence_id(&self) -> u64 {
self.sequence_count.fetch_add(1, Ordering::Relaxed)
}
pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) {
if let Some(thread_state) = self.thread_states().get_mut(&thread_id) {
thread_state.status = status;
};
}
pub fn thread_states(&self) -> MutexGuard<HashMap<u64, ThreadState>> {
self.thread_states.lock()
}
pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState {
self.thread_states.lock().get(&thread_id).cloned().unwrap()
}
pub async fn initialize(&self) -> Result<dap_types::Capabilities> {
let args = dap_types::InitializeRequestArguments {
client_id: Some("zed".to_owned()),
client_name: Some("Zed".to_owned()),
adapter_id: self.config.id.clone(),
locale: Some("en-us".to_owned()),
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
supports_variable_type: Some(true),
supports_variable_paging: Some(false),
supports_run_in_terminal_request: Some(false), // TODO: we should support this
supports_memory_references: Some(true),
supports_progress_reporting: Some(true),
supports_invalidated_event: Some(false),
lines_start_at1: Some(true),
columns_start_at1: Some(true),
supports_memory_event: Some(true),
supports_args_can_be_interpreted_by_shell: None,
supports_start_debugging_request: Some(true),
};
let capabilities = self.request::<Initialize>(args).await?;
*self.capabilities.lock() = Some(capabilities.clone());
Ok(capabilities)
}
pub async fn launch(&self, args: Option<Value>) -> Result<()> {
self.request::<Launch>(LaunchRequestArguments {
raw: args.unwrap_or(Value::Null),
})
.await
}
pub async fn attach(&self, args: Option<Value>) -> Result<()> {
self.request::<Attach>(AttachRequestArguments {
raw: args.unwrap_or(Value::Null),
})
.await
}
pub async fn resume(&self, thread_id: u64) -> Result<ContinueResponse> {
self.request::<Continue>(ContinueArguments {
thread_id,
single_thread: Some(true),
})
.await
}
pub async fn step_over(&self, thread_id: u64) -> Result<()> {
self.request::<Next>(NextArguments {
thread_id,
granularity: Some(SteppingGranularity::Statement),
single_thread: Some(true),
})
.await
}
pub async fn step_in(&self, thread_id: u64) -> Result<()> {
self.request::<StepIn>(StepInArguments {
thread_id,
target_id: None,
granularity: Some(SteppingGranularity::Statement),
single_thread: Some(true),
})
.await
}
pub async fn step_out(&self, thread_id: u64) -> Result<()> {
self.request::<StepOut>(StepOutArguments {
thread_id,
granularity: Some(SteppingGranularity::Statement),
single_thread: Some(true),
})
.await
}
pub async fn step_back(&self, thread_id: u64) -> Result<()> {
self.request::<StepBack>(StepBackArguments {
thread_id,
single_thread: Some(true),
granularity: Some(SteppingGranularity::Statement),
})
.await
}
pub async fn restart(&self) {
self.request::<Restart>(RestartArguments {
raw: self
.config
.request_args
.as_ref()
.map(|v| v.args.clone())
.unwrap_or(Value::Null),
})
.await
.log_err();
}
pub async fn pause(&self, thread_id: u64) {
self.request::<Pause>(PauseArguments { thread_id })
.await
.log_err();
}
pub async fn stop(&self) {
self.request::<Disconnect>(DisconnectArguments {
restart: Some(false),
terminate_debuggee: Some(false),
suspend_debuggee: Some(false),
})
.await
.log_err();
}
pub async fn set_breakpoints(
&self,
path: PathBuf,
breakpoints: Option<Vec<SourceBreakpoint>>,
) -> Result<SetBreakpointsResponse> {
let adapter_data = self.config.request_args.clone().map(|c| c.args);
self.request::<SetBreakpoints>(SetBreakpointsArguments {
source: Source {
path: Some(String::from(path.to_string_lossy())),
name: None,
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data,
checksums: None,
},
breakpoints,
source_modified: None,
lines: None,
})
.await
}
pub async fn configuration_done(&self) -> Result<()> {
self.request::<ConfigurationDone>(ConfigurationDoneArguments)
.await
}
}

3
crates/dap/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod client;
pub mod transport;
pub use dap_types::*;

283
crates/dap/src/transport.rs Normal file
View File

@@ -0,0 +1,283 @@
use anyhow::{anyhow, Context, Result};
use dap_types::{
BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent,
InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent,
ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent,
ThreadEvent,
};
use futures::{AsyncBufRead, AsyncWrite};
use gpui::AsyncAppContext;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use smol::{
channel::{unbounded, Receiver, Sender},
io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt},
lock::Mutex,
};
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Payload {
Event(Box<Events>),
Response(Response),
Request(Request),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "event", content = "body")]
#[serde(rename_all = "camelCase")]
pub enum Events {
Initialized(Option<Capabilities>),
Stopped(StoppedEvent),
Continued(ContinuedEvent),
Exited(ExitedEvent),
Terminated(Option<TerminatedEvent>),
Thread(ThreadEvent),
Output(OutputEvent),
Breakpoint(BreakpointEvent),
Module(ModuleEvent),
LoadedSource(LoadedSourceEvent),
Process(ProcessEvent),
Capabilities(CapabilitiesEvent),
ProgressStart(ProgressStartEvent),
ProgressUpdate(ProgressUpdateEvent),
ProgressEnd(ProgressEndEvent),
Invalidated(InvalidatedEvent),
Memory(MemoryEvent),
#[serde(untagged)]
Other(HashMap<String, Value>),
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Request {
#[serde(skip)]
pub back_ch: Option<Sender<Result<Response>>>,
pub seq: u64,
pub command: String,
pub arguments: Option<Value>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Response {
pub request_seq: u64,
pub success: bool,
pub command: String,
pub message: Option<String>,
#[serde(default, deserialize_with = "deserialize_empty_object")]
pub body: Option<Value>,
}
fn deserialize_empty_object<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
if value == Value::Object(serde_json::Map::new()) {
Ok(None)
} else {
Ok(Some(value))
}
}
#[derive(Debug)]
pub struct Transport {
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
}
impl Transport {
pub fn start(
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
cx: &mut AsyncAppContext,
) -> (Receiver<Payload>, Sender<Payload>) {
let (client_tx, server_rx) = unbounded::<Payload>();
let (server_tx, client_rx) = unbounded::<Payload>();
let transport = Arc::new(Self {
pending_requests: Mutex::new(HashMap::default()),
});
let _ = cx.update(|cx| {
let transport = transport.clone();
cx.background_executor()
.spawn(Self::receive(transport.clone(), server_stdout, client_tx))
.detach_and_log_err(cx);
cx.background_executor()
.spawn(Self::send(transport.clone(), server_stdin, client_rx))
.detach_and_log_err(cx);
if let Some(stderr) = server_stderr {
cx.background_executor()
.spawn(Self::err(stderr))
.detach_and_log_err(cx);
}
});
(server_rx, server_tx)
}
async fn recv_server_message(
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
buffer: &mut String,
) -> Result<Payload> {
let mut content_length = None;
loop {
buffer.truncate(0);
if reader
.read_line(buffer)
.await
.with_context(|| "reading a message from server")?
== 0
{
return Err(anyhow!("reader stream closed"));
};
if buffer == "\r\n" {
break;
}
let parts = buffer.trim().split_once(": ");
match parts {
Some(("Content-Length", value)) => {
content_length = Some(value.parse().context("invalid content length")?);
}
_ => {}
}
}
let content_length = content_length.context("missing content length")?;
let mut content = vec![0; content_length];
reader
.read_exact(&mut content)
.await
.with_context(|| "reading after a loop")?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
Ok(serde_json::from_str::<Payload>(msg)?)
}
async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String,
) -> Result<()> {
buffer.truncate(0);
if err.read_line(buffer).await? == 0 {
return Err(anyhow!("error stream closed"));
};
Ok(())
}
async fn send_payload_to_server(
&self,
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
mut payload: Payload,
) -> Result<()> {
if let Payload::Request(request) = &mut payload {
if let Some(back) = request.back_ch.take() {
self.pending_requests.lock().await.insert(request.seq, back);
}
}
self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?)
.await
}
async fn send_string_to_server(
&self,
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
request: String,
) -> Result<()> {
server_stdin
.write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes())
.await?;
server_stdin.flush().await?;
Ok(())
}
fn process_response(response: Response) -> Result<Response> {
if response.success {
Ok(response)
} else {
Err(anyhow!("Received failed response"))
}
}
async fn process_server_message(
&self,
client_tx: &Sender<Payload>,
payload: Payload,
) -> Result<()> {
match payload {
Payload::Response(res) => {
if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) {
if !tx.is_closed() {
tx.send(Self::process_response(res)).await?;
} else {
log::warn!(
"Response stream associated with request seq: {} is closed",
&res.request_seq
); // TODO: Fix this case so it never happens
}
} else {
client_tx.send(Payload::Response(res)).await?;
};
}
Payload::Request(_) => {
client_tx.send(payload).await?;
}
Payload::Event(_) => {
client_tx.send(payload).await?;
}
}
Ok(())
}
async fn receive(
transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: Sender<Payload>,
) -> Result<()> {
let mut recv_buffer = String::new();
loop {
transport
.process_server_message(
&client_tx,
Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?,
)
.await
.context("Process server message failed in transport::receive")?;
}
}
async fn send(
transport: Arc<Self>,
mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
client_rx: Receiver<Payload>,
) -> Result<()> {
while let Ok(payload) = client_rx.recv().await {
transport
.send_payload_to_server(&mut server_stdin, payload)
.await?;
}
Ok(())
}
async fn err(mut server_stderr: Box<dyn AsyncBufRead + Unpin + Send>) -> Result<()> {
let mut recv_buffer = String::new();
loop {
Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?;
}
}
}

View File

@@ -0,0 +1,32 @@
[package]
name = "debugger_ui"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
dap.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
picker.workspace = true
project.workspace = true
serde.workspace = true
serde_derive.workspace = true
task.workspace = true
tasks_ui.workspace = true
ui.workspace = true
workspace.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,602 @@
use anyhow::Result;
use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus};
use dap::requests::{Disconnect, Scopes, StackTrace, Variables};
use dap::{client::DebugAdapterClient, transport::Events};
use dap::{
Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments,
StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
Variable, VariablesArguments,
};
use editor::Editor;
use futures::future::try_join_all;
use gpui::{
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
Subscription, Task, View, ViewContext, WeakView,
};
use std::path::Path;
use std::{collections::HashMap, sync::Arc};
use task::DebugRequestType;
use ui::prelude::*;
use workspace::Pane;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
use crate::debugger_panel_item::DebugPanelItem;
enum DebugCurrentRowHighlight {}
#[derive(Debug)]
pub enum DebugPanelEvent {
Stopped((DebugAdapterClientId, StoppedEvent)),
Thread((DebugAdapterClientId, ThreadEvent)),
}
actions!(debug_panel, [ToggleFocus]);
pub struct DebugPanel {
size: Pixels,
pane: View<Pane>,
focus_handle: FocusHandle,
workspace: WeakView<Workspace>,
_subscriptions: Vec<Subscription>,
}
impl DebugPanel {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
cx.new_view(|cx| {
let pane = cx.new_view(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
workspace.project().clone(),
Default::default(),
None,
None,
cx,
);
pane.set_can_split(false, cx);
pane.set_can_navigate(true, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_| true);
pane
});
let project = workspace.project().clone();
let _subscriptions = vec![cx.subscribe(&project, {
move |this: &mut Self, _, event, cx| match event {
project::Event::DebugClientEvent { event, client_id } => {
Self::handle_debug_client_events(
this,
this.debug_client_by_id(*client_id, cx),
event,
cx,
);
}
project::Event::DebugClientStarted(client_id) => {
let client = this.debug_client_by_id(*client_id, cx);
cx.spawn(|_, _| async move {
client.initialize().await?;
let request_args = client.config().request_args.map(|a| a.args);
// send correct request based on adapter config
match client.config().request {
DebugRequestType::Launch => client.launch(request_args).await,
DebugRequestType::Attach => client.attach(request_args).await,
}
})
.detach_and_log_err(cx);
}
_ => {}
}
})];
Self {
pane,
size: px(300.),
_subscriptions,
focus_handle: cx.focus_handle(),
workspace: workspace.weak_handle(),
}
})
}
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
})
}
fn debug_client_by_id(
&self,
client_id: DebugAdapterClientId,
cx: &mut ViewContext<Self>,
) -> Arc<DebugAdapterClient> {
self.workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.debug_adapter_by_id(client_id)
.unwrap()
})
.unwrap()
}
fn handle_debug_client_events(
this: &mut Self,
client: Arc<DebugAdapterClient>,
event: &Events,
cx: &mut ViewContext<Self>,
) {
match event {
Events::Initialized(event) => Self::handle_initialized_event(client, event, cx),
Events::Stopped(event) => Self::handle_stopped_event(client, event, cx),
Events::Continued(event) => Self::handle_continued_event(client, event, cx),
Events::Exited(event) => Self::handle_exited_event(client, event, cx),
Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx),
Events::Thread(event) => Self::handle_thread_event(client, event, cx),
Events::Output(_) => {}
Events::Breakpoint(_) => {}
Events::Module(_) => {}
Events::LoadedSource(_) => {}
Events::Capabilities(_) => {}
Events::Memory(_) => {}
Events::Process(_) => {}
Events::ProgressEnd(_) => {}
Events::ProgressStart(_) => {}
Events::ProgressUpdate(_) => {}
Events::Invalidated(_) => {}
Events::Other(_) => {}
}
}
pub async fn go_to_stack_frame(
workspace: WeakView<Workspace>,
stack_frame: StackFrame,
client: Arc<DebugAdapterClient>,
clear_highlights: bool,
mut cx: AsyncWindowContext,
) -> Result<()> {
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
let row = (stack_frame.line.saturating_sub(1)) as u32;
let column = (stack_frame.column.saturating_sub(1)) as u32;
if clear_highlights {
Self::remove_highlights(workspace.clone(), client, cx.clone()).await?;
}
let task = workspace.update(&mut cx, |workspace, cx| {
let project_path = workspace.project().read_with(cx, |project, cx| {
project.project_path_for_absolute_path(&Path::new(&path), cx)
});
if let Some(project_path) = project_path {
workspace.open_path_preview(project_path, None, false, true, cx)
} else {
Task::ready(Err(anyhow::anyhow!(
"No project path found for path: {}",
path
)))
}
})?;
let editor = task.await?.downcast::<Editor>().unwrap();
workspace.update(&mut cx, |_, cx| {
editor.update(cx, |editor, cx| {
editor.go_to_line::<DebugCurrentRowHighlight>(
row,
column,
Some(cx.theme().colors().editor_debugger_active_line_background),
cx,
);
})
})
}
async fn remove_highlights(
workspace: WeakView<Workspace>,
client: Arc<DebugAdapterClient>,
cx: AsyncWindowContext,
) -> Result<()> {
let mut tasks = Vec::new();
for thread_state in client.thread_states().values() {
for stack_frame in thread_state.stack_frames.clone() {
tasks.push(Self::remove_editor_highlight(
workspace.clone(),
stack_frame,
cx.clone(),
));
}
}
if !tasks.is_empty() {
try_join_all(tasks).await?;
}
anyhow::Ok(())
}
async fn remove_highlights_for_thread(
workspace: WeakView<Workspace>,
client: Arc<DebugAdapterClient>,
thread_id: u64,
cx: AsyncWindowContext,
) -> Result<()> {
let mut tasks = Vec::new();
if let Some(thread_state) = client.thread_states().get(&thread_id) {
for stack_frame in thread_state.stack_frames.clone() {
tasks.push(Self::remove_editor_highlight(
workspace.clone(),
stack_frame.clone(),
cx.clone(),
));
}
}
if !tasks.is_empty() {
try_join_all(tasks).await?;
}
anyhow::Ok(())
}
async fn remove_editor_highlight(
workspace: WeakView<Workspace>,
stack_frame: StackFrame,
mut cx: AsyncWindowContext,
) -> Result<()> {
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
let task = workspace.update(&mut cx, |workspace, cx| {
let project_path = workspace.project().read_with(cx, |project, cx| {
project.project_path_for_absolute_path(&Path::new(&path), cx)
});
if let Some(project_path) = project_path {
workspace.open_path(project_path, None, false, cx)
} else {
Task::ready(Err(anyhow::anyhow!(
"No project path found for path: {}",
path
)))
}
})?;
let editor = task.await?.downcast::<Editor>().unwrap();
editor.update(&mut cx, |editor, _| {
editor.clear_row_highlights::<DebugCurrentRowHighlight>();
})
}
fn handle_initialized_event(
client: Arc<DebugAdapterClient>,
_: &Option<Capabilities>,
cx: &mut ViewContext<Self>,
) {
cx.spawn(|this, mut cx| async move {
let task = this.update(&mut cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
let client = client.clone();
project.send_breakpoints(client, cx)
})
})
})??;
task.await?;
client.configuration_done().await
})
.detach_and_log_err(cx);
}
fn handle_continued_event(
client: Arc<DebugAdapterClient>,
event: &ContinuedEvent,
cx: &mut ViewContext<Self>,
) {
let all_threads = event.all_threads_continued.unwrap_or(false);
if all_threads {
for thread in client.thread_states().values_mut() {
thread.status = ThreadStatus::Running;
}
} else {
client.update_thread_state_status(event.thread_id, ThreadStatus::Running);
}
cx.notify();
}
fn handle_stopped_event(
client: Arc<DebugAdapterClient>,
event: &StoppedEvent,
cx: &mut ViewContext<Self>,
) {
let Some(thread_id) = event.thread_id else {
return;
};
let client_id = client.id();
cx.spawn({
let event = event.clone();
|this, mut cx| async move {
let stack_trace_response = client
.request::<StackTrace>(StackTraceArguments {
thread_id,
start_frame: None,
levels: None,
format: None,
})
.await?;
let current_stack_frame =
stack_trace_response.stack_frames.first().unwrap().clone();
let mut scope_tasks = Vec::new();
for stack_frame in stack_trace_response.stack_frames.clone().into_iter() {
let frame_id = stack_frame.id;
let client = client.clone();
scope_tasks.push(async move {
anyhow::Ok((
frame_id,
client
.request::<Scopes>(ScopesArguments { frame_id })
.await?,
))
});
}
let mut scopes: HashMap<u64, Vec<Scope>> = HashMap::new();
let mut variables: HashMap<u64, Vec<Variable>> = HashMap::new();
let mut variable_tasks = Vec::new();
for (thread_id, response) in try_join_all(scope_tasks).await? {
scopes.insert(thread_id, response.scopes.clone());
for scope in response.scopes {
let scope_reference = scope.variables_reference;
let client = client.clone();
variable_tasks.push(async move {
anyhow::Ok((
scope_reference,
client
.request::<Variables>(VariablesArguments {
variables_reference: scope_reference,
filter: None,
start: None,
count: None,
format: None,
})
.await?,
))
});
}
}
for (scope_reference, response) in try_join_all(variable_tasks).await? {
variables.insert(scope_reference, response.variables.clone());
}
this.update(&mut cx, |this, cx| {
let mut thread_state = client.thread_states();
let thread_state = thread_state
.entry(thread_id)
.or_insert(ThreadState::default());
thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id);
thread_state.stack_frames = stack_trace_response.stack_frames;
thread_state.scopes = scopes;
thread_state.variables = variables;
thread_state.status = ThreadStatus::Stopped;
let existing_item = this
.pane
.read(cx)
.items()
.filter_map(|item| item.downcast::<DebugPanelItem>())
.any(|item| {
let item = item.read(cx);
item.client().id() == client_id && item.thread_id() == thread_id
});
if !existing_item {
let debug_panel = cx.view().clone();
this.pane.update(cx, |this, cx| {
let tab = cx.new_view(|cx| {
DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx)
});
this.add_item(Box::new(tab.clone()), false, false, None, cx)
});
}
cx.emit(DebugPanelEvent::Stopped((client_id, event)));
if let Some(item) = this.pane.read(cx).active_item() {
if let Some(pane) = item.downcast::<DebugPanelItem>() {
let pane = pane.read(cx);
if pane.thread_id() == thread_id && pane.client().id() == client_id {
let workspace = this.workspace.clone();
let client = client.clone();
return cx.spawn(|_, cx| async move {
Self::go_to_stack_frame(
workspace,
current_stack_frame.clone(),
client,
true,
cx,
)
.await
});
}
}
}
Task::ready(anyhow::Ok(()))
})?
.await
}
})
.detach_and_log_err(cx);
}
fn handle_thread_event(
client: Arc<DebugAdapterClient>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
) {
let thread_id = event.thread_id;
if event.reason == ThreadEventReason::Started {
client
.thread_states()
.insert(thread_id, ThreadState::default());
} else {
client.update_thread_state_status(thread_id, ThreadStatus::Ended);
// TODO: we want to figure out for witch clients/threads we should remove the highlights
cx.spawn({
let client = client.clone();
|this, mut cx| async move {
let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?;
Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?;
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
}
cx.emit(DebugPanelEvent::Thread((client.id(), event.clone())));
}
fn handle_exited_event(
client: Arc<DebugAdapterClient>,
_: &ExitedEvent,
cx: &mut ViewContext<Self>,
) {
cx.spawn(|_, _| async move {
for thread_state in client.thread_states().values_mut() {
thread_state.status = ThreadStatus::Exited;
}
})
.detach();
}
fn handle_terminated_event(
this: &mut Self,
client: Arc<DebugAdapterClient>,
event: &Option<TerminatedEvent>,
cx: &mut ViewContext<Self>,
) {
let restart_args = event.clone().and_then(|e| e.restart);
let workspace = this.workspace.clone();
cx.spawn(|_, cx| async move {
let should_restart = restart_args.is_some();
Self::remove_highlights(workspace, client.clone(), cx).await?;
client
.request::<Disconnect>(DisconnectArguments {
restart: Some(should_restart),
terminate_debuggee: None,
suspend_debuggee: None,
})
.await?;
if should_restart {
match client.request_type() {
DebugRequestType::Launch => client.launch(restart_args).await,
DebugRequestType::Attach => client.attach(restart_args).await,
}
} else {
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
}
}
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
impl EventEmitter<project::Event> for DebugPanel {}
impl FocusableView for DebugPanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Panel for DebugPanel {
fn persistent_name() -> &'static str {
"DebugPanel"
}
fn position(&self, _cx: &WindowContext) -> DockPosition {
DockPosition::Bottom
}
fn position_is_valid(&self, position: DockPosition) -> bool {
position == DockPosition::Bottom
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn size(&self, _cx: &WindowContext) -> Pixels {
self.size
}
fn set_size(&mut self, size: Option<Pixels>, _cx: &mut ViewContext<Self>) {
self.size = size.unwrap();
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
None
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
None
}
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn icon_label(&self, _: &WindowContext) -> Option<String> {
None
}
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
false
}
fn starts_open(&self, _cx: &WindowContext) -> bool {
false
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
}
impl Render for DebugPanel {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.key_context("DebugPanel")
.track_focus(&self.focus_handle)
.size_full()
.child(self.pane.clone())
.into_any()
}
}

View File

@@ -0,0 +1,514 @@
use crate::debugger_panel::{DebugPanel, DebugPanelEvent};
use anyhow::Result;
use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus};
use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable};
use gpui::{
actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, ListState, Subscription, View, WeakView,
};
use std::sync::Arc;
use ui::WindowContext;
use ui::{prelude::*, Tooltip};
use workspace::item::{Item, ItemEvent};
pub struct DebugPanelItem {
thread_id: u64,
focus_handle: FocusHandle,
stack_frame_list: ListState,
client: Arc<DebugAdapterClient>,
_subscriptions: Vec<Subscription>,
current_stack_frame_id: Option<u64>,
}
actions!(
debug_panel_item,
[Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop]
);
impl DebugPanelItem {
pub fn new(
debug_panel: View<DebugPanel>,
client: Arc<DebugAdapterClient>,
thread_id: u64,
cx: &mut ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let weakview = cx.view().downgrade();
let stack_frame_list =
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
if let Some(view) = weakview.upgrade() {
view.update(cx, |view, cx| {
view.render_stack_frame(ix, cx).into_any_element()
})
} else {
div().into_any()
}
});
let _subscriptions = vec![cx.subscribe(&debug_panel, {
move |this: &mut Self, _, event: &DebugPanelEvent, cx| {
match event {
DebugPanelEvent::Stopped((client_id, event)) => {
Self::handle_stopped_event(this, client_id, event, cx)
}
DebugPanelEvent::Thread((client_id, event)) => {
Self::handle_thread_event(this, client_id, event, cx)
}
};
}
})];
Self {
client,
thread_id,
focus_handle,
_subscriptions,
stack_frame_list,
current_stack_frame_id: None,
}
}
fn should_skip_event(
this: &mut Self,
client_id: &DebugAdapterClientId,
thread_id: u64,
) -> bool {
thread_id != this.thread_id || *client_id != this.client.id()
}
fn handle_stopped_event(
this: &mut Self,
client_id: &DebugAdapterClientId,
event: &StoppedEvent,
cx: &mut ViewContext<Self>,
) {
if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) {
return;
}
this.stack_frame_list
.reset(this.current_thread_state().stack_frames.len());
cx.notify();
}
fn handle_thread_event(
this: &mut Self,
client_id: &DebugAdapterClientId,
event: &ThreadEvent,
_: &mut ViewContext<Self>,
) {
if Self::should_skip_event(this, client_id, event.thread_id) {
return;
}
// TODO: handle thread event
}
}
impl EventEmitter<ItemEvent> for DebugPanelItem {}
impl FocusableView for DebugPanelItem {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for DebugPanelItem {
type Event = ItemEvent;
fn tab_content(
&self,
params: workspace::item::TabContentParams,
_: &WindowContext,
) -> AnyElement {
Label::new(format!(
"{} - Thread {}",
self.client.config().id,
self.thread_id
))
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
Some(SharedString::from(format!(
"{} Thread {} - {:?}",
self.client.config().id,
self.thread_id,
self.current_thread_state().status
)))
}
}
impl DebugPanelItem {
pub fn client(&self) -> Arc<DebugAdapterClient> {
self.client.clone()
}
pub fn thread_id(&self) -> u64 {
self.thread_id
}
fn stack_frame_for_index(&self, ix: usize) -> StackFrame {
self.client
.thread_state_by_id(self.thread_id)
.stack_frames
.get(ix)
.cloned()
.unwrap()
}
fn current_thread_state(&self) -> ThreadState {
self.client
.thread_states()
.get(&self.thread_id)
.cloned()
.unwrap()
}
fn render_stack_frames(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_1_3()
.gap_3()
.h_full()
.child(list(self.stack_frame_list.clone()).size_full())
.into_any()
}
fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let stack_frame = self.stack_frame_for_index(ix);
let source = stack_frame.source.clone();
let selected_frame_id = self.current_stack_frame_id;
let is_selected_frame = Some(stack_frame.id) == selected_frame_id;
let formatted_path = format!(
"{}:{}",
source.clone().and_then(|s| s.name).unwrap_or_default(),
stack_frame.line,
);
v_flex()
.rounded_md()
.group("")
.id(("stack-frame", stack_frame.id))
.tooltip({
let formatted_path = formatted_path.clone();
move |cx| Tooltip::text(formatted_path.clone(), cx)
})
.p_1()
.when(is_selected_frame, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_click(cx.listener({
let stack_frame = stack_frame.clone();
move |this, _, _| {
this.current_stack_frame_id = Some(stack_frame.id);
// let client = this.client();
// DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx)
// .detach_and_log_err(cx);
// TODO:
// this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx)
// .detach_and_log_err(cx);
// cx.notify();
}
}))
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
h_flex()
.gap_0p5()
.text_ui_sm(cx)
.child(stack_frame.name.clone())
.child(formatted_path),
)
.child(
h_flex()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
)
.into_any()
}
fn render_scopes(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let thread_state = self.current_thread_state();
let Some(scopes) = thread_state
.current_stack_frame_id
.and_then(|id| thread_state.scopes.get(&id))
else {
return div().child("No scopes for this thread yet").into_any();
};
div()
.w_3_4()
.gap_3()
.text_ui_sm(cx)
.children(
scopes
.iter()
.map(|scope| self.render_scope(&thread_state, scope, cx)),
)
.into_any()
}
fn render_scope(
&self,
thread_state: &ThreadState,
scope: &Scope,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
div()
.id(("scope", scope.variables_reference))
.p_1()
.text_ui_sm(cx)
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(scope.name.clone())
.child(
div()
.ml_2()
.child(self.render_variables(thread_state, scope, cx)),
)
.into_any()
}
fn render_variables(
&self,
thread_state: &ThreadState,
scope: &Scope,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let Some(variables) = thread_state.variables.get(&scope.variables_reference) else {
return div().child("No variables for this thread yet").into_any();
};
div()
.gap_3()
.text_ui_sm(cx)
.children(
variables
.iter()
.map(|variable| self.render_variable(variable, cx)),
)
.into_any()
}
fn render_variable(&self, variable: &Variable, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.id(("variable", variable.variables_reference))
.p_1()
.gap_1()
.text_ui_sm(cx)
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(variable.name.clone())
.child(
div()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
.child(variable.value.clone()),
)
.into_any()
}
// if the debug adapter does not send the continued event,
// and the status of the thread did not change we have to assume the thread is running
// so we have to update the thread state status to running
fn update_thread_state(
this: WeakView<Self>,
previous_status: ThreadStatus,
all_threads_continued: Option<bool>,
mut cx: AsyncWindowContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if previous_status == this.current_thread_state().status {
if all_threads_continued.unwrap_or(false) {
for thread in this.client.thread_states().values_mut() {
thread.status = ThreadStatus::Running;
}
} else {
this.client
.update_thread_state_status(this.thread_id, ThreadStatus::Running);
}
cx.notify();
}
})
}
fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let thread_id = self.thread_id;
let previous_status = self.current_thread_state().status;
cx.spawn(|this, cx| async move {
let response = client.resume(thread_id).await?;
Self::update_thread_state(this, previous_status, response.all_threads_continued, cx)
})
.detach_and_log_err(cx);
}
fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let thread_id = self.thread_id;
let previous_status = self.current_thread_state().status;
cx.spawn(|this, cx| async move {
client.step_over(thread_id).await?;
Self::update_thread_state(this, previous_status, None, cx)
})
.detach_and_log_err(cx);
}
fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let thread_id = self.thread_id;
let previous_status = self.current_thread_state().status;
cx.spawn(|this, cx| async move {
client.step_in(thread_id).await?;
Self::update_thread_state(this, previous_status, None, cx)
})
.detach_and_log_err(cx);
}
fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let thread_id = self.thread_id;
let previous_status = self.current_thread_state().status;
cx.spawn(|this, cx| async move {
client.step_out(thread_id).await?;
Self::update_thread_state(this, previous_status, None, cx)
})
.detach_and_log_err(cx);
}
fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
cx.background_executor()
.spawn(async move { client.restart().await })
.detach();
}
fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let thread_id = self.thread_id;
cx.background_executor()
.spawn(async move { client.pause(thread_id).await })
.detach();
}
fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
cx.background_executor()
.spawn(async move { client.stop().await })
.detach();
}
}
impl Render for DebugPanelItem {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let thread_status = self.current_thread_state().status;
v_flex()
.key_context("DebugPanelItem")
.track_focus(&self.focus_handle)
.capture_action(cx.listener(Self::handle_continue_action))
.capture_action(cx.listener(Self::handle_step_over_action))
.capture_action(cx.listener(Self::handle_step_in_action))
.capture_action(cx.listener(Self::handle_step_out_action))
.capture_action(cx.listener(Self::handle_restart_action))
.capture_action(cx.listener(Self::handle_pause_action))
.capture_action(cx.listener(Self::handle_stop_action))
.p_2()
.size_full()
.items_start()
.child(
h_flex()
.gap_2()
.map(|this| {
if self.current_thread_state().status == ThreadStatus::Running {
this.child(
IconButton::new("debug-pause", IconName::DebugPause)
.on_click(
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))),
)
.tooltip(move |cx| Tooltip::text("Pause program", cx)),
)
} else {
this.child(
IconButton::new("debug-continue", IconName::DebugContinue)
.on_click(cx.listener(|_, _, cx| {
cx.dispatch_action(Box::new(Continue))
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |cx| Tooltip::text("Continue program", cx)),
)
}
})
.child(
IconButton::new("debug-step-over", IconName::DebugStepOver)
.on_click(
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))),
)
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |cx| Tooltip::text("Step over", cx)),
)
.child(
IconButton::new("debug-step-in", IconName::DebugStepInto)
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn))))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |cx| Tooltip::text("Step in", cx)),
)
.child(
IconButton::new("debug-step-out", IconName::DebugStepOut)
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut))))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |cx| Tooltip::text("Step out", cx)),
)
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart))))
.disabled(
!self
.client
.capabilities()
.supports_restart_request
.unwrap_or_default()
|| thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip(move |cx| Tooltip::text("Restart", cx)),
)
.child(
IconButton::new("debug-stop", IconName::DebugStop)
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip(move |cx| Tooltip::text("Stop", cx)),
),
)
.child(
h_flex()
.size_full()
.items_start()
.p_1()
.gap_4()
.child(self.render_stack_frames(cx))
.child(self.render_scopes(cx)),
)
.into_any()
}
}

View File

@@ -0,0 +1,18 @@
use debugger_panel::{DebugPanel, ToggleFocus};
use gpui::AppContext;
use workspace::{StartDebugger, Workspace};
pub mod debugger_panel;
mod debugger_panel_item;
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<DebugPanel>(cx);
});
workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| {
tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach();
});
})
.detach();
}

View File

@@ -293,6 +293,7 @@ gpui::actions!(
SplitSelectionIntoLines,
Tab,
TabPrev,
ToggleBreakpoint,
ToggleAutoSignatureHelp,
ToggleGitBlame,
ToggleGitBlameInline,

View File

@@ -109,7 +109,6 @@ pub struct DisplayMap {
crease_map: CreaseMap,
fold_placeholder: FoldPlaceholder,
pub clip_at_line_ends: bool,
pub(crate) masked: bool,
}
impl DisplayMap {
@@ -157,7 +156,6 @@ impl DisplayMap {
text_highlights: Default::default(),
inlay_highlights: Default::default(),
clip_at_line_ends: false,
masked: false,
}
}
@@ -184,7 +182,6 @@ impl DisplayMap {
text_highlights: self.text_highlights.clone(),
inlay_highlights: self.inlay_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
masked: self.masked,
fold_placeholder: self.fold_placeholder.clone(),
}
}
@@ -502,7 +499,6 @@ pub struct DisplaySnapshot {
text_highlights: TextHighlights,
inlay_highlights: InlayHighlights,
clip_at_line_ends: bool,
masked: bool,
pub(crate) fold_placeholder: FoldPlaceholder,
}
@@ -654,7 +650,6 @@ impl DisplaySnapshot {
.chunks(
display_row.0..self.max_point().row().next_row().0,
false,
self.masked,
Highlights::default(),
)
.map(|h| h.text)
@@ -662,9 +657,9 @@ impl DisplaySnapshot {
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
(0..=display_row.0).rev().flat_map(move |row| {
(0..=display_row.0).rev().flat_map(|row| {
self.block_snapshot
.chunks(row..row + 1, false, self.masked, Highlights::default())
.chunks(row..row + 1, false, Highlights::default())
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -681,7 +676,6 @@ impl DisplaySnapshot {
self.block_snapshot.chunks(
display_rows.start.0..display_rows.end.0,
language_aware,
self.masked,
Highlights {
text_highlights: Some(&self.text_highlights),
inlay_highlights: Some(&self.inlay_highlights),

View File

@@ -23,7 +23,6 @@ use text::Edit;
use ui::ElementId;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
const BULLETS: &str = "********************************************************************************************************************************";
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
///
@@ -286,7 +285,6 @@ pub struct BlockChunks<'a> {
input_chunk: Chunk<'a>,
output_row: u32,
max_output_row: u32,
masked: bool,
}
#[derive(Clone)]
@@ -895,7 +893,6 @@ impl BlockSnapshot {
self.chunks(
0..self.transforms.summary().output_rows,
false,
false,
Highlights::default(),
)
.map(|chunk| chunk.text)
@@ -906,7 +903,6 @@ impl BlockSnapshot {
&'a self,
rows: Range<u32>,
language_aware: bool,
masked: bool,
highlights: Highlights<'a>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
@@ -945,7 +941,6 @@ impl BlockSnapshot {
transforms: cursor,
output_row: rows.start,
max_output_row,
masked,
}
}
@@ -1234,20 +1229,12 @@ impl<'a> Iterator for BlockChunks<'a> {
let (prefix_rows, prefix_bytes) =
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
self.output_row += prefix_rows;
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
self.input_chunk.text = suffix;
if self.output_row == transform_end {
self.transforms.next(&());
}
if self.masked {
// Not great for multibyte text because to keep cursor math correct we
// need to have the same number of bytes in the input as output.
let chars = prefix.chars().count();
let bullet_len = chars;
prefix = &BULLETS[..bullet_len];
}
Some(Chunk {
text: prefix,
..self.input_chunk.clone()
@@ -2061,7 +2048,6 @@ mod tests {
.chunks(
start_row as u32..blocks_snapshot.max_point().row + 1,
false,
false,
Highlights::default(),
)
.map(|chunk| chunk.text)

View File

@@ -408,7 +408,6 @@ impl EditorActionId {
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
#[derive(Default)]
struct ScrollbarMarkerState {
scrollbar_size: Size<Pixels>,
dirty: bool,
@@ -422,6 +421,17 @@ impl ScrollbarMarkerState {
}
}
impl Default for ScrollbarMarkerState {
fn default() -> Self {
Self {
scrollbar_size: Size::default(),
dirty: false,
markers: Arc::from([]),
pending_refresh: None,
}
}
}
#[derive(Clone, Debug)]
struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>,
@@ -439,6 +449,13 @@ struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: Anchor,
}
#[derive(Clone, Debug)]
struct Breakpoint {
row: MultiBufferRow,
_line: BufferRow,
}
#[derive(Copy, Clone, Debug)]
struct MultiBufferOffset(usize);
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
@@ -480,6 +497,7 @@ pub struct Editor {
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
redact_all: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
@@ -557,6 +575,7 @@ pub struct Editor {
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
file_header_size: u8,
breadcrumb_header: Option<String>,
@@ -1802,6 +1821,7 @@ impl Editor {
show_code_actions: None,
show_runnables: None,
show_wrap_guides: None,
redact_all: false,
show_indent_guides,
placeholder_text: None,
highlight_order: 0,
@@ -1871,6 +1891,7 @@ impl Editor {
blame_subscription: None,
file_header_size,
tasks: Default::default(),
breakpoints: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -5120,6 +5141,17 @@ impl Editor {
}
}
fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext<Self>) -> IconButton {
IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.icon_color(Color::Error)
.on_click(cx.listener(move |editor, _e, cx| {
editor.focus(cx);
editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded
}))
}
fn render_run_indicator(
&self,
_style: &EditorStyle,
@@ -5718,7 +5750,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
let empty_str: Arc<str> = Arc::default();
let empty_str: Arc<str> = "".into();
buffer.edit(
deletion_ranges
.into_iter()
@@ -5784,7 +5816,7 @@ impl Editor {
self.transact(cx, |this, cx| {
let buffer = this.buffer.update(cx, |buffer, cx| {
let empty_str: Arc<str> = Arc::default();
let empty_str: Arc<str> = "".into();
buffer.edit(
edit_ranges
.into_iter()
@@ -5938,6 +5970,38 @@ impl Editor {
}
}
pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext<Self>) {
let cursor_position: Point = self.selections.newest(cx).head();
self.toggle_breakpoint_at_row(cursor_position.row, cx);
}
pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext<Self>) {
let Some(project) = &self.project else {
return;
};
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
return;
};
let buffer_id = buffer.read(cx).remote_id();
let key = (buffer_id, row);
if self.breakpoints.remove(&key).is_none() {
self.breakpoints.insert(
key,
Breakpoint {
row: MultiBufferRow(row),
_line: row,
},
);
}
project.update(cx, |project, cx| {
project.update_breakpoint(buffer, row + 1, cx);
});
cx.notify();
}
fn gather_revert_changes(
&mut self,
selections: &[Selection<Anchor>],
@@ -8085,7 +8149,7 @@ impl Editor {
let mut selection_edit_ranges = Vec::new();
let mut last_toggled_row = None;
let snapshot = this.buffer.read(cx).read(cx);
let empty_str: Arc<str> = Arc::default();
let empty_str: Arc<str> = "".into();
let mut suffixes_inserted = Vec::new();
fn comment_prefix_range(
@@ -8914,6 +8978,31 @@ impl Editor {
}
}
pub fn go_to_line<T: 'static>(
&mut self,
row: u32,
column: u32,
highlight_color: Option<Hsla>,
cx: &mut ViewContext<Self>,
) {
let snapshot = self.snapshot(cx).display_snapshot;
let point = snapshot
.buffer_snapshot
.clip_point(Point::new(row, column), Bias::Left);
let anchor = snapshot.buffer_snapshot.anchor_before(point);
self.clear_row_highlights::<T>();
self.highlight_rows::<T>(
anchor..=anchor,
Some(
highlight_color
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
),
true,
cx,
);
self.request_autoscroll(Autoscroll::center(), cx);
}
fn seek_in_direction(
&mut self,
snapshot: &DisplaySnapshot,
@@ -10418,11 +10507,9 @@ impl Editor {
cx.notify();
}
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
if self.display_map.read(cx).masked != masked {
self.display_map.update(cx, |map, _| map.masked = masked);
}
cx.notify()
pub fn set_redact_all(&mut self, redact_all: bool, cx: &mut ViewContext<Self>) {
self.redact_all = redact_all;
cx.notify();
}
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
@@ -11108,6 +11195,10 @@ impl Editor {
display_snapshot: &DisplaySnapshot,
cx: &WindowContext,
) -> Vec<Range<DisplayPoint>> {
if self.redact_all {
return vec![DisplayPoint::zero()..display_snapshot.max_point()];
}
display_snapshot
.buffer_snapshot
.redacted_ranges(search_range, |file| {
@@ -12430,7 +12521,6 @@ impl Render for Editor {
color: cx.theme().colors().editor_foreground,
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(settings.buffer_line_height.value()),
@@ -12440,7 +12530,6 @@ impl Render for Editor {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),

View File

@@ -305,7 +305,7 @@ pub struct ScrollbarContent {
}
/// Gutter related settings
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///

View File

@@ -1,6 +1,4 @@
use std::sync::Arc;
use gpui::{AppContext, FontFeatures, FontWeight};
use gpui::{AppContext, FontWeight};
use project::project_settings::{InlineBlameSettings, ProjectSettings};
use settings::{EditableSettingControl, Settings};
use theme::{FontFamilyCache, ThemeSettings};
@@ -9,8 +7,6 @@ use ui::{
SettingsGroup,
};
use crate::EditorSettings;
#[derive(IntoElement)]
pub struct EditorSettingsControls {}
@@ -32,19 +28,9 @@ impl RenderOnce for EditorSettingsControls {
.child(BufferFontFamilyControl)
.child(BufferFontWeightControl),
)
.child(BufferFontSizeControl)
.child(BufferFontLigaturesControl),
.child(BufferFontSizeControl),
)
.child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
.child(
SettingsGroup::new("Gutter").child(
h_flex()
.gap_2()
.justify_between()
.child(LineNumbersControl)
.child(RelativeLineNumbersControl),
),
)
}
}
@@ -204,76 +190,6 @@ impl RenderOnce for BufferFontWeightControl {
}
}
#[derive(IntoElement)]
struct BufferFontLigaturesControl;
impl EditableSettingControl for BufferFontLigaturesControl {
type Value = bool;
type Settings = ThemeSettings;
fn name(&self) -> SharedString {
"Buffer Font Ligatures".into()
}
fn read(cx: &AppContext) -> Self::Value {
let settings = ThemeSettings::get_global(cx);
settings
.buffer_font
.features
.is_calt_enabled()
.unwrap_or(true)
}
fn apply(
settings: &mut <Self::Settings as Settings>::FileContent,
value: Self::Value,
_cx: &AppContext,
) {
let value = if value { 1 } else { 0 };
let mut features = settings
.buffer_font_features
.as_ref()
.map(|features| {
features
.tag_value_list()
.into_iter()
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
features[calt_index].1 = value;
} else {
features.push(("calt".into(), value));
}
settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
}
}
impl RenderOnce for BufferFontLigaturesControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
CheckboxWithLabel::new(
"buffer-font-ligatures",
Label::new(self.name()),
value.into(),
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);
},
)
}
}
#[derive(IntoElement)]
struct InlineGitBlameControl;
@@ -326,102 +242,3 @@ impl RenderOnce for InlineGitBlameControl {
)
}
}
#[derive(IntoElement)]
struct LineNumbersControl;
impl EditableSettingControl for LineNumbersControl {
type Value = bool;
type Settings = EditorSettings;
fn name(&self) -> SharedString {
"Line Numbers".into()
}
fn read(cx: &AppContext) -> Self::Value {
let settings = EditorSettings::get_global(cx);
settings.gutter.line_numbers
}
fn apply(
settings: &mut <Self::Settings as Settings>::FileContent,
value: Self::Value,
_cx: &AppContext,
) {
if let Some(gutter) = settings.gutter.as_mut() {
gutter.line_numbers = Some(value);
} else {
settings.gutter = Some(crate::editor_settings::GutterContent {
line_numbers: Some(value),
..Default::default()
});
}
}
}
impl RenderOnce for LineNumbersControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
CheckboxWithLabel::new(
"line-numbers",
Label::new(self.name()),
value.into(),
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);
},
)
}
}
#[derive(IntoElement)]
struct RelativeLineNumbersControl;
impl EditableSettingControl for RelativeLineNumbersControl {
type Value = bool;
type Settings = EditorSettings;
fn name(&self) -> SharedString {
"Relative Line Numbers".into()
}
fn read(cx: &AppContext) -> Self::Value {
let settings = EditorSettings::get_global(cx);
settings.relative_line_numbers
}
fn apply(
settings: &mut <Self::Settings as Settings>::FileContent,
value: Self::Value,
_cx: &AppContext,
) {
settings.relative_line_numbers = Some(value);
}
}
impl RenderOnce for RelativeLineNumbersControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
DropdownMenu::new(
"relative-line-numbers",
if value { "Relative" } else { "Ascending" },
ContextMenu::build(cx, |menu, _cx| {
menu.custom_entry(
|_cx| Label::new("Ascending").into_any_element(),
move |cx| Self::write(false, cx),
)
.custom_entry(
|_cx| Label::new("Relative").into_any_element(),
move |cx| Self::write(true, cx),
)
}),
)
}
}

View File

@@ -10105,7 +10105,7 @@ struct Row8;
struct Row9;
struct Row10;"#};
// Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
// Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
assert_hunk_revert(
indoc! {r#"struct Row;
struct Row2;

View File

@@ -59,7 +59,6 @@ use std::{
fmt::{self, Write},
iter, mem,
ops::{Deref, Range},
rc::Rc,
sync::Arc,
};
use sum_tree::Bias;
@@ -401,7 +400,8 @@ impl EditorElement {
register_action(view, cx, Editor::accept_partial_inline_completion);
register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal)
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::toggle_breakpoint);
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
@@ -1555,6 +1555,45 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
fn layout_breakpoints(
&self,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
editor
.breakpoints
.iter()
.filter_map(|(_, breakpoint)| {
if snapshot.is_line_folded(breakpoint.row) {
return None;
}
let display_row = Point::new(breakpoint.row.0, 0)
.to_display_point(snapshot)
.row();
let button = editor.render_breakpoint(display_row, cx);
let button = prepaint_gutter_button(
button,
display_row,
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
rows_with_hunk_bounds,
cx,
);
Some(button)
})
.collect_vec()
})
}
fn layout_run_indicators(
&self,
line_height: Pixels,
@@ -1970,7 +2009,6 @@ impl EditorElement {
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
}))
.cursor(CursorStyle::Arrow)
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.into_any_element()
}
@@ -3254,6 +3292,9 @@ impl EditorElement {
}
});
for breakpoint in layout.breakpoints.iter_mut() {
breakpoint.paint(cx);
}
for test_indicator in layout.test_indicators.iter_mut() {
test_indicator.paint(cx);
}
@@ -4107,11 +4148,11 @@ fn prepaint_gutter_button(
);
let indicator_size = button.layout_as_root(available_space, cx);
let blame_width = gutter_dimensions.git_blame_entries_width;
let gutter_width = rows_with_hunk_bounds
let blame_offset = gutter_dimensions.git_blame_entries_width;
let gutter_offset = rows_with_hunk_bounds
.get(&row)
.map(|bounds| bounds.size.width);
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
.map(|bounds| bounds.origin.x + bounds.size.width);
let left_offset = blame_offset.max(gutter_offset).unwrap_or(Pixels::ZERO);
let mut x = left_offset;
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
@@ -5394,6 +5435,16 @@ impl Element for EditorElement {
}
}
let breakpoints = self.layout_breakpoints(
line_height,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&rows_with_hunk_bounds,
&snapshot,
cx,
);
let test_indicators = if gutter_settings.runnables {
self.layout_run_indicators(
line_height,
@@ -5494,7 +5545,7 @@ impl Element for EditorElement {
EditorLayout {
mode: snapshot.mode,
position_map: Rc::new(PositionMap {
position_map: Arc::new(PositionMap {
size: bounds.size,
scroll_pixel_position,
scroll_max,
@@ -5529,6 +5580,7 @@ impl Element for EditorElement {
selections,
mouse_context_menu,
test_indicators,
breakpoints,
close_indicators,
code_actions_indicator,
gutter_fold_toggles,
@@ -5644,7 +5696,7 @@ impl IntoElement for EditorElement {
}
pub struct EditorLayout {
position_map: Rc<PositionMap>,
position_map: Arc<PositionMap>,
hitbox: Hitbox,
text_hitbox: Hitbox,
gutter_hitbox: Hitbox,
@@ -5671,6 +5723,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
breakpoints: Vec<AnyElement>,
close_indicators: Vec<AnyElement>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,

View File

@@ -13,8 +13,8 @@ use multi_buffer::{
use settings::SettingsStore;
use text::{BufferId, Point};
use ui::{
div, h_flex, rems, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement,
IntoElement, ParentElement, Pixels, Styled, ViewContext, VisualContext,
div, h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
ParentElement, Pixels, Styled, ViewContext, VisualContext,
};
use util::{debug_panic, RangeExt};
@@ -484,10 +484,7 @@ impl Editor {
.child(
h_flex()
.id("gutter hunk")
.pl(gutter_dimensions.margin
+ gutter_dimensions
.git_blame_entries_width
.unwrap_or_default())
.pl(hunk_bounds.origin.x)
.max_w(hunk_bounds.size.width)
.min_w(hunk_bounds.size.width)
.size_full()
@@ -515,7 +512,7 @@ impl Editor {
.child(
v_flex()
.size_full()
.pt(rems(0.25))
.pt(ui::rems(0.25))
.justify_start()
.child(close_button),
),

View File

@@ -44,7 +44,7 @@ impl SelectionsCollection {
buffer,
next_selection_id: 1,
line_mode: false,
disjoint: Arc::default(),
disjoint: Arc::from([]),
pending: Some(PendingSelection {
selection: Selection {
id: 0,
@@ -398,7 +398,7 @@ impl<'a> MutableSelectionsCollection<'a> {
}
pub fn clear_disjoint(&mut self) {
self.collection.disjoint = Arc::default();
self.collection.disjoint = Arc::from([]);
}
pub fn delete(&mut self, selection_id: usize) {

View File

@@ -3,7 +3,7 @@ use crate::Editor;
use gpui::{Task as AsyncTask, WindowContext};
use project::Location;
use task::{TaskContext, TaskVariables, VariableName};
use text::{ToOffset, ToPoint};
use text::{Point, ToOffset, ToPoint};
use workspace::Workspace;
fn task_context_with_editor(
@@ -14,7 +14,11 @@ fn task_context_with_editor(
return AsyncTask::ready(None);
};
let (selection, buffer, editor_snapshot) = {
let selection = editor.selections.newest_adjusted(cx);
let mut selection = editor.selections.newest::<Point>(cx);
if editor.selections.line_mode {
selection.start = Point::new(selection.start.row, 0);
selection.end = Point::new(selection.end.row + 1, 0);
}
let Some((buffer, _, _)) = editor
.buffer()
.read(cx)

View File

@@ -27,7 +27,6 @@ pub fn marked_display_snapshot(
let font = Font {
family: "Zed Plex Mono".into(),
features: FontFeatures::default(),
fallbacks: None,
weight: FontWeight::default(),
style: FontStyle::default(),
};

View File

@@ -327,7 +327,7 @@ impl EditorTestContext {
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_else(|| Arc::default())
.unwrap_or_else(|| Arc::from([]))
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()

View File

@@ -21,6 +21,7 @@ assistant_slash_command.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
cap-std.workspace = true
client.workspace = true
collections.workspace = true
fs.workspace = true

View File

@@ -363,7 +363,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
},
);
#[allow(clippy::let_underscore_future)]
let _ = store.update(cx, |store, cx| store.reload(None, cx));
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);

View File

@@ -159,25 +159,29 @@ impl WasmHost {
}
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
use cap_std::{ambient_authority, fs::Dir};
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
self.fs
.create_dir(&extension_work_dir)
.await
.context("failed to create extension work dir")?;
let file_perms = wasi::FilePerms::all();
let work_dir_preopen = Dir::open_ambient_dir(&extension_work_dir, ambient_authority())
.context("failed to preopen extension work directory")?;
let current_dir_preopen = work_dir_preopen
.try_clone()
.context("failed to preopen extension current directory")?;
let extension_work_dir = extension_work_dir.to_string_lossy();
let perms = wasi::FilePerms::all();
let dir_perms = wasi::DirPerms::all();
Ok(wasi::WasiCtxBuilder::new()
.inherit_stdio()
.preopened_dir(&extension_work_dir, ".", dir_perms, file_perms)?
.preopened_dir(
&extension_work_dir,
&extension_work_dir.to_string_lossy(),
dir_perms,
file_perms,
)?
.env("PWD", &extension_work_dir.to_string_lossy())
.preopened_dir(current_dir_preopen, dir_perms, perms, ".")
.preopened_dir(work_dir_preopen, dir_perms, perms, &extension_work_dir)
.env("PWD", &extension_work_dir)
.env("RUST_BACKTRACE", "full")
.build())
}

View File

@@ -29,7 +29,7 @@ pub fn new_linker(
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
) -> Linker<WasmState> {
let mut linker = Linker::new(&wasm_engine());
wasmtime_wasi::add_to_linker_async(&mut linker).unwrap();
wasmtime_wasi::command::add_to_linker(&mut linker).unwrap();
f(&mut linker, wasi_view).unwrap();
linker
}

View File

@@ -12,7 +12,6 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.1",
with: {
"worktree": ExtensionWorktree,

View File

@@ -11,7 +11,6 @@ pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.4",
with: {
"worktree": ExtensionWorktree,

View File

@@ -12,7 +12,6 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.6",
with: {
"worktree": ExtensionWorktree,

View File

@@ -26,7 +26,6 @@ pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.0.7",
with: {
"worktree": ExtensionWorktree,

View File

@@ -816,7 +816,6 @@ impl ExtensionsPage {
},
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),

View File

@@ -998,7 +998,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1006,7 +1006,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1014,7 +1014,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1022,7 +1022,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1030,7 +1030,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
];
@@ -1044,7 +1044,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1052,7 +1052,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1060,7 +1060,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1068,7 +1068,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
@@ -1076,7 +1076,7 @@ mod tests {
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
]

View File

@@ -404,12 +404,7 @@ mod tests {
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec![
"aαbβ/cγ",
"αβγδ/bcde",
"c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f",
"/d/🆒/h",
];
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_single_path_query("bcd", false, &paths),

View File

@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
worktree_id,
positions: Vec::new(),
path: Arc::from(candidate.path),
path_prefix: Arc::default(),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
);

View File

@@ -5,9 +5,6 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/google_ai.rs"

View File

@@ -73,7 +73,7 @@ thiserror.workspace = true
time.workspace = true
util.workspace = true
uuid.workspace = true
waker-fn = "1.2.0"
waker-fn = "1.1.0"
[dev-dependencies]
backtrace = "0.3"
@@ -93,7 +93,6 @@ cbindgen = { version = "0.26.0", default-features = false }
block = "0.1"
cocoa.workspace = true
core-foundation.workspace = true
core-foundation-sys = "0.8"
core-graphics = "0.23"
core-text = "20.1"
foreign-types = "0.5"
@@ -151,7 +150,7 @@ x11-clipboard = "0.9.2"
[target.'cfg(windows)'.dependencies]
windows.workspace = true
windows-core = "0.58"
windows-core = "0.57"
[[example]]
name = "hello_world"

View File

@@ -1,50 +0,0 @@
use gpui::{
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
};
use std::path::PathBuf;
struct GifViewer {
gif_path: PathBuf,
}
impl GifViewer {
fn new(gif_path: PathBuf) -> Self {
Self { gif_path }
}
}
impl Render for GifViewer {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child(
img(ImageSource::File(self.gif_path.clone().into()))
.size_full()
.object_fit(gpui::ObjectFit::Contain)
.id("gif"),
)
}
}
fn main() {
env_logger::init();
App::new().run(|cx: &mut AppContext| {
let cwd = std::env::current_dir().expect("Failed to get current working directory");
let gif_path = cwd.join("crates/gpui/examples/image/black-cat-typing.gif");
if !gif_path.exists() {
eprintln!("Image file not found at {:?}", gif_path);
eprintln!("Make sure you're running this example from the root of the gpui crate");
cx.quit();
return;
}
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|cx| cx.new_view(|_cx| GifViewer::new(gif_path)),
)
.unwrap();
cx.activate(true);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

View File

@@ -1,7 +1,6 @@
use crate::{size, DevicePixels, Result, SharedString, Size};
use smallvec::SmallVec;
use image::{Delay, Frame};
use image::RgbaImage;
use std::{
borrow::Cow,
fmt,
@@ -35,54 +34,43 @@ pub struct ImageId(usize);
#[derive(PartialEq, Eq, Hash, Clone)]
pub(crate) struct RenderImageParams {
pub(crate) image_id: ImageId,
pub(crate) frame_index: usize,
}
/// A cached and processed image.
pub struct ImageData {
/// The ID associated with this image
pub id: ImageId,
data: SmallVec<[Frame; 1]>,
data: RgbaImage,
}
impl ImageData {
/// Create a new image from the given data.
pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
pub fn new(data: RgbaImage) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
data: data.into(),
data,
}
}
/// Convert this image into a byte slice.
pub fn as_bytes(&self, frame_index: usize) -> &[u8] {
&self.data[frame_index].buffer()
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Get the size of this image, in pixels.
pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
let (width, height) = self.data[frame_index].buffer().dimensions();
/// Get the size of this image, in pixels
pub fn size(&self) -> Size<DevicePixels> {
let (width, height) = self.data.dimensions();
size(width.into(), height.into())
}
/// Get the delay of this frame from the previous
pub fn delay(&self, frame_index: usize) -> Delay {
self.data[frame_index].delay()
}
/// Get the number of frames for this image.
pub fn frame_count(&self) -> usize {
self.data.len()
}
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
.field("size", &self.size(0))
.field("size", &self.data.dimensions())
.finish()
}
}

View File

@@ -323,14 +323,14 @@ impl Interactivity {
pub fn on_boxed_action(
&mut self,
action: &dyn Action,
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
) {
let action = action.boxed_clone();
self.action_listeners.push((
(*action).type_id(),
Box::new(move |_, phase, cx| {
if phase == DispatchPhase::Bubble {
(listener)(&*action, cx)
(listener)(&action, cx)
}
}),
));
@@ -757,7 +757,7 @@ pub trait InteractiveElement: Sized {
fn on_boxed_action(
mut self,
action: &dyn Action,
listener: impl Fn(&dyn Action, &mut WindowContext) + 'static,
listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().on_boxed_action(action, listener);
self

View File

@@ -1,3 +1,7 @@
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use crate::{
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
@@ -5,20 +9,11 @@ use crate::{
WindowContext,
};
use futures::{AsyncReadExt, Future};
use http_client;
use image::{
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
};
use image::{ImageBuffer, ImageError};
#[cfg(target_os = "macos")]
use media::core_video::CVImageBuffer;
use smallvec::SmallVec;
use std::{
fs,
io::Cursor,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
use http_client;
use thiserror::Error;
use util::ResultExt;
@@ -235,14 +230,8 @@ impl Img {
}
}
/// The image state between frames
struct ImgState {
frame_index: usize,
last_frame_time: Option<Instant>,
}
impl Element for Img {
type RequestLayoutState = usize;
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
@@ -254,65 +243,29 @@ impl Element for Img {
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
cx.with_optional_element_state(global_id, |state, cx| {
let mut state = state.map(|state| {
state.unwrap_or(ImgState {
frame_index: 0,
last_frame_time: None,
})
});
let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
let layout_id = self
.interactivity
.request_layout(global_id, cx, |mut style, cx| {
if let Some(data) = self.source.data(cx) {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
if elapsed >= frame_duration {
state.frame_index = (state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
}
} else {
state.last_frame_time = Some(current_time);
}
let layout_id = self
.interactivity
.request_layout(global_id, cx, |mut style, cx| {
if let Some(data) = self.source.data(cx) {
let image_size = data.size();
match (style.size.width, style.size.height) {
(Length::Auto, Length::Auto) => {
style.size = Size {
width: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
)),
height: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
)),
}
}
let image_size = data.size(frame_index);
match (style.size.width, style.size.height) {
(Length::Auto, Length::Auto) => {
style.size = Size {
width: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
)),
height: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
)),
}
}
_ => {}
}
if global_id.is_some() && data.frame_count() > 1 {
cx.request_animation_frame();
}
_ => {}
}
}
cx.request_layout(style, [])
});
((layout_id, frame_index), state)
})
cx.request_layout(style, [])
});
(layout_id, ())
}
fn prepaint(
@@ -330,7 +283,7 @@ impl Element for Img {
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
frame_index: &mut Self::RequestLayoutState,
_: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
@@ -340,15 +293,9 @@ impl Element for Img {
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
if let Some(data) = source.data(cx) {
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
cx.paint_image(
new_bounds,
corner_radii,
data.clone(),
*frame_index,
self.grayscale,
)
.log_err();
let new_bounds = self.object_fit.get_bounds(bounds, data.size());
cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
.log_err();
}
match source {
@@ -438,34 +385,12 @@ impl Asset for Image {
};
let data = if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
let mut frames = SmallVec::new();
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
for frame in decoder.into_frames() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
}
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
};
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
ImageData::new(data)
} else {
@@ -475,7 +400,7 @@ impl Asset for Image {
let buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
ImageData::new(buffer)
};
Ok(Arc::new(data))

View File

@@ -180,7 +180,7 @@ impl Transformation {
}
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
//Note: if you read this as a sequence of matrix mulitplications, start from the bottom
TransformationMatrix::unit()
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
.rotate(self.rotate)

View File

@@ -325,9 +325,7 @@ impl UniformList {
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
let Some(mut item_to_measure) = items.pop() else {
return Size::default();
};
let mut item_to_measure = items.pop().unwrap();
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)

View File

@@ -940,15 +940,6 @@ where
pub fn half_perimeter(&self) -> T {
self.size.width.clone() + self.size.height.clone()
}
/// centered_at creates a new bounds centered at the given point.
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
let origin = Point {
x: center.x - size.width.half(),
y: center.y - size.height.half(),
};
Self::new(origin, size)
}
}
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {

View File

@@ -4,6 +4,9 @@
mod app_menu;
mod keystroke;
#[cfg(not(target_os = "macos"))]
mod cosmic_text;
#[cfg(target_os = "linux")]
mod linux;
@@ -48,6 +51,8 @@ use uuid::Uuid;
pub use app_menu::*;
pub use keystroke::*;
#[cfg(not(target_os = "macos"))]
pub(crate) use cosmic_text::*;
#[cfg(target_os = "linux")]
pub(crate) use linux::*;
#[cfg(target_os = "macos")]
@@ -100,6 +105,7 @@ pub fn guess_compositor() -> &'static str {
}
}
// todo("windows")
#[cfg(target_os = "windows")]
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(WindowsPlatform::new())
@@ -407,6 +413,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
#[cfg(target_os = "windows")]
fn destroy(&self);
}
#[derive(PartialEq, Eq, Hash, Clone)]
@@ -706,6 +714,7 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub window_min_size: Option<Size<Pixels>>,
}

View File

@@ -0,0 +1,3 @@
mod text_system;
pub(crate) use text_system::*;

View File

@@ -64,17 +64,13 @@ impl PlatformTextSystem for CosmicTextSystem {
}
fn all_font_names(&self) -> Vec<String> {
let mut result = self
.0
self.0
.read()
.font_system
.db()
.faces()
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
.collect_vec();
result.sort();
result.dedup();
result
.map(|face| face.post_script_name.clone())
.collect()
}
fn all_font_families(&self) -> Vec<String> {
@@ -181,6 +177,9 @@ impl PlatformTextSystem for CosmicTextSystem {
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, runs)
}
#[cfg(target_os = "windows")]
fn destroy(&self) {}
}
impl CosmicTextSystemState {

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