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
43 changed files with 2828 additions and 30 deletions

1
.gitignore vendored
View File

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

55
Cargo.lock generated
View File

@@ -3224,6 +3224,37 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "dap"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"dap-types",
"futures 0.3.28",
"gpui",
"log",
"parking_lot",
"postage",
"release_channel",
"schemars",
"serde",
"serde_json",
"serde_json_lenient",
"smol",
"task",
"util",
]
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types#e715437f1193d5da6d7de6a71abdd1ac1fbf4c9d"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "dashmap"
version = "5.5.3"
@@ -3273,6 +3304,27 @@ dependencies = [
"util",
]
[[package]]
name = "debugger_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"dap",
"db",
"editor",
"futures 0.3.28",
"fuzzy",
"gpui",
"picker",
"project",
"serde",
"serde_derive",
"task",
"tasks_ui",
"ui",
"workspace",
]
[[package]]
name = "deflate64"
version = "0.1.8"
@@ -7962,6 +8014,7 @@ dependencies = [
"client",
"clock",
"collections",
"dap",
"dev_server_projects",
"env_logger",
"fs",
@@ -10672,6 +10725,7 @@ dependencies = [
"parking_lot",
"schemars",
"serde",
"serde_json",
"serde_json_lenient",
"sha2 0.10.7",
"shellexpand 2.1.2",
@@ -13627,6 +13681,7 @@ dependencies = [
"command_palette",
"copilot",
"db",
"debugger_ui",
"dev_server_projects",
"diagnostics",
"editor",

View File

@@ -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",
@@ -183,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" }

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

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

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

@@ -449,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)]
@@ -568,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>,
@@ -1883,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),
@@ -5132,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,
@@ -5950,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>],
@@ -8926,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,

View File

@@ -400,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) {
@@ -1554,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,
@@ -3252,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);
}
@@ -5392,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,
@@ -5527,6 +5580,7 @@ impl Element for EditorElement {
selections,
mouse_context_menu,
test_indicators,
breakpoints,
close_indicators,
code_actions_indicator,
gutter_fold_toggles,
@@ -5669,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

@@ -249,3 +249,15 @@ pub fn local_tasks_file_relative_path() -> &'static Path {
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
Path::new(".vscode/tasks.json")
}
/// Returns the relative path to a `launch.json` file within a project.
pub fn local_debug_file_relative_path() -> &'static Path {
static LOCAL_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
LOCAL_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/debug.json"))
}
/// Returns the relative path to a `.vscode/launch.json` file within a project.
pub fn local_vscode_launch_file_relative_path() -> &'static Path {
static LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/launch.json"))
}

View File

@@ -30,6 +30,7 @@ async-trait.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
dap.workspace = true
dev_server_projects.workspace = true
fs.workspace = true
futures.workspace = true

View File

@@ -24,6 +24,11 @@ use client::{
};
use clock::ReplicaId;
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use dap::{
client::{DebugAdapterClient, DebugAdapterClientId},
transport::Events,
SourceBreakpoint,
};
use debounced_delay::DebouncedDelay;
use futures::{
channel::mpsc::{self, UnboundedReceiver},
@@ -51,8 +56,8 @@ use language::{
deserialize_anchor, deserialize_version, serialize_anchor, serialize_line_ending,
serialize_version, split_operations,
},
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel,
ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
range_from_lsp, Bias, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability,
CodeLabel, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
ToPointUtf16, Transaction, Unclipped,
@@ -68,7 +73,8 @@ use lsp_command::*;
use node_runtime::NodeRuntime;
use parking_lot::{Mutex, RwLock};
use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path,
local_debug_file_relative_path, local_settings_file_relative_path,
local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
local_vscode_tasks_file_relative_path,
};
use postage::watch;
@@ -175,6 +181,8 @@ pub struct Project {
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
language_servers: HashMap<LanguageServerId, LanguageServerState>,
language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
debug_adapters: HashMap<DebugAdapterClientId, DebugAdapterClientState>,
breakpoints: HashMap<BufferId, Vec<Breakpoint>>,
language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
last_formatting_failure: Option<String>,
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
@@ -183,6 +191,7 @@ pub struct Project {
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
client: Arc<client::Client>,
next_entry_id: Arc<AtomicUsize>,
next_debugger_id: AtomicUsize,
join_project_response_message_id: u32,
next_diagnostic_group_id: usize,
diagnostic_summaries:
@@ -306,6 +315,10 @@ impl PartialEq for LanguageServerPromptRequest {
}
}
struct Breakpoint {
row: BufferRow,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
LanguageServerAdded(LanguageServerId),
@@ -314,6 +327,11 @@ pub enum Event {
Notification(String),
LanguageServerPrompt(LanguageServerPromptRequest),
LanguageNotFound(Model<Buffer>),
DebugClientStarted(DebugAdapterClientId),
DebugClientEvent {
client_id: DebugAdapterClientId,
event: Events,
},
ActiveEntryChanged(Option<ProjectEntryId>),
ActivateProjectPanel,
WorktreeAdded,
@@ -379,6 +397,11 @@ pub struct LanguageServerProgress {
pub last_update_at: Instant,
}
pub enum DebugAdapterClientState {
Starting(Task<Option<Arc<DebugAdapterClient>>>),
Running(Arc<DebugAdapterClient>),
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ProjectPath {
pub worktree_id: WorktreeId,
@@ -800,11 +823,14 @@ impl Project {
fs,
ssh_session: None,
next_entry_id: Default::default(),
next_debugger_id: Default::default(),
next_diagnostic_group_id: Default::default(),
diagnostics: Default::default(),
diagnostic_summaries: Default::default(),
supplementary_language_servers: HashMap::default(),
language_servers: Default::default(),
debug_adapters: Default::default(),
breakpoints: Default::default(),
language_server_ids: HashMap::default(),
language_server_statuses: Default::default(),
last_formatting_failure: None,
@@ -962,6 +988,7 @@ impl Project {
fs,
ssh_session: None,
next_entry_id: Default::default(),
next_debugger_id: Default::default(),
next_diagnostic_group_id: Default::default(),
diagnostic_summaries: Default::default(),
diagnostics: Default::default(),
@@ -997,6 +1024,8 @@ impl Project {
)
})
.collect(),
debug_adapters: Default::default(),
breakpoints: Default::default(),
last_formatting_failure: None,
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: HashMap::default(),
@@ -1106,6 +1135,177 @@ impl Project {
}
}
pub fn running_debug_adapters(&self) -> impl Iterator<Item = Arc<DebugAdapterClient>> + '_ {
self.debug_adapters
.values()
.filter_map(|state| match state {
DebugAdapterClientState::Starting(_) => None,
DebugAdapterClientState::Running(client) => Some(client.clone()),
})
}
pub fn debug_adapter_by_id(&self, id: DebugAdapterClientId) -> Option<Arc<DebugAdapterClient>> {
self.debug_adapters.get(&id).and_then(|state| match state {
DebugAdapterClientState::Starting(_) => None,
DebugAdapterClientState::Running(client) => Some(client.clone()),
})
}
pub fn send_breakpoints(
&self,
client: Arc<DebugAdapterClient>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
cx.spawn(|project, mut cx| async move {
let task = project.update(&mut cx, |project, cx| {
let mut tasks = Vec::new();
for (buffer_id, breakpoints) in project.breakpoints.iter() {
let res = maybe!({
let buffer = project.buffer_for_id(*buffer_id, cx)?;
let project_path = buffer.read(cx).project_path(cx)?;
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
let path = worktree.read(cx).absolutize(&project_path.path).ok()?;
Some((path, breakpoints))
});
if let Some((path, breakpoints)) = res {
tasks.push(
client.set_breakpoints(
path,
Some(
breakpoints
.iter()
.map(|b| SourceBreakpoint {
line: b.row as u64,
condition: None,
hit_condition: None,
log_message: None,
column: None,
mode: None,
})
.collect::<Vec<_>>(),
),
),
);
}
}
try_join_all(tasks)
})?;
task.await?;
Ok(())
})
}
pub fn start_debug_adapter_client(
&mut self,
debug_task: task::ResolvedTask,
cx: &mut ModelContext<Self>,
) {
let id = DebugAdapterClientId(self.next_debugger_id());
let debug_template = debug_task.original_task();
let cwd = debug_template
.cwd
.clone()
.expect("Debug tasks need to know what directory to open");
let adapter_config = debug_task
.debug_adapter_config()
.expect("Debug tasks need to specify adapter configuration");
let command = debug_template.command.clone();
let args = debug_template.args.clone();
let task = cx.spawn(|this, mut cx| async move {
let project = this.clone();
let client = DebugAdapterClient::new(
id,
adapter_config.clone(),
&command,
args.iter().map(|ele| &ele[..]).collect(),
cwd.into(),
move |event, cx| {
project
.update(cx, |_, cx| {
cx.emit(Event::DebugClientEvent {
client_id: id,
event,
})
})
.log_err();
},
&mut cx,
)
.await
.log_err()?;
this.update(&mut cx, |this, cx| {
let handle = this
.debug_adapters
.get_mut(&id)
.with_context(|| "Failed to find debug adapter with given id")?;
*handle = DebugAdapterClientState::Running(client.clone());
cx.emit(Event::DebugClientStarted(id));
anyhow::Ok(())
})
.log_err();
Some(client)
});
self.debug_adapters
.insert(id, DebugAdapterClientState::Starting(task));
}
pub fn update_breakpoint(
&mut self,
buffer: Model<Buffer>,
row: BufferRow,
cx: &mut ModelContext<Self>,
) {
let breakpoints_for_buffer = self
.breakpoints
.entry(buffer.read(cx).remote_id())
.or_insert(Vec::new());
if let Some(ix) = breakpoints_for_buffer
.iter()
.position(|breakpoint| breakpoint.row == row)
{
breakpoints_for_buffer.remove(ix);
} else {
breakpoints_for_buffer.push(Breakpoint { row });
}
let clients = self
.debug_adapters
.iter()
.filter_map(|(_, state)| match state {
DebugAdapterClientState::Starting(_) => None,
DebugAdapterClientState::Running(client) => Some(client.clone()),
})
.collect::<Vec<_>>();
let mut tasks = Vec::new();
for client in clients {
tasks.push(self.send_breakpoints(client, cx));
}
cx.background_executor()
.spawn(async move {
try_join_all(tasks).await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn shutdown_language_servers(
&mut self,
_cx: &mut ModelContext<Self>,
@@ -8123,17 +8323,35 @@ impl Project {
abs_path,
id_base: "local_vscode_tasks_for_worktree".into(),
},
|tx, cx| {
StaticSource::new(TrackedFile::new_convertible::<
task::VsCodeTaskFile,
>(
tasks_file_rx, tx, cx
))
},
|tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
cx,
);
}
})
} else if path.ends_with(local_debug_file_relative_path()) {
self.task_inventory().update(cx, |task_inventory, cx| {
if removed {
task_inventory.remove_local_static_source(&abs_path);
} else {
let fs = self.fs.clone();
let debug_task_file_rx =
watch_config_file(&cx.background_executor(), fs, abs_path.clone());
task_inventory.add_source(
TaskSourceKind::Worktree {
id: remote_worktree_id,
abs_path,
id_base: "local_debug_file_for_worktree".into(),
},
|tx, cx| {
StaticSource::new(TrackedFile::new(debug_task_file_rx, tx, cx))
},
cx,
);
}
});
} else if path.ends_with(local_vscode_launch_file_relative_path()) {
// TODO: handle vscode launch file (.vscode/launch.json)
}
}
@@ -8357,6 +8575,37 @@ impl Project {
})
}
pub fn project_path_for_absolute_path(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<ProjectPath> {
self.find_local_worktree(abs_path, cx)
.map(|(worktree, relative_path)| ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
})
}
pub fn find_local_worktree(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<(Model<Worktree>, PathBuf)> {
let trees = self.worktrees(cx);
for tree in trees {
if let Some(relative_path) = tree
.read(cx)
.as_local()
.and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
{
return Some((tree.clone(), relative_path.into()));
}
}
None
}
pub fn get_workspace_root(
&self,
project_path: &ProjectPath,
@@ -10253,6 +10502,10 @@ impl Project {
}
}
fn next_debugger_id(&mut self) -> usize {
self.next_debugger_id.fetch_add(1, SeqCst)
}
pub fn task_context_for_location(
&self,
captured_variables: TaskVariables,
@@ -10434,6 +10687,7 @@ impl Project {
hide,
shell,
tags: proto_template.tags,
..Default::default()
};
Some((task_source_kind, task_template))
})

View File

@@ -17,6 +17,7 @@ hex.workspace = true
parking_lot.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
sha2.workspace = true
shellexpand.workspace = true

View File

@@ -0,0 +1,24 @@
use anyhow::bail;
use collections::HashMap;
use serde::Deserialize;
use util::ResultExt;
use crate::{TaskTemplate, TaskTemplates, VariableName};
struct ZedDebugTaskFile {}
impl ZedDebugTaskFile {
fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {}
}
impl TryFrom<ZedDebugTaskFile> for TaskTemplates {
type Error = anyhow::Error;
fn try_from(value: ZedDebugTaskFile) -> Result<Self, Self::Error> {
let templates = value
.tasks
.into_iter()
.filter_map(|debug_task_file| debug_task_file.to_zed_format(&replacer).log_err())
.collect();
Ok(Self(templates))
}
}

View File

@@ -13,7 +13,10 @@ use std::path::PathBuf;
use std::str::FromStr;
use std::{borrow::Cow, path::Path};
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
pub use task_template::{
DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TCPHost,
TaskTemplate, TaskTemplates, TaskType, HideStrategy,
};
pub use vscode_format::VsCodeTaskFile;
/// Task identifier, unique within the application.
@@ -111,6 +114,17 @@ impl ResolvedTask {
&self.original_task
}
/// Get the task type that determines what this task is used for
/// And where is it shown in the UI
pub fn task_type(&self) -> TaskType {
self.original_task.task_type.clone()
}
/// Get the configuration for the debug adapter that should be used for this task.
pub fn debug_adapter_config(&self) -> Option<DebugAdapterConfig> {
self.original_task.debug_adapter.clone()
}
/// Variables that were substituted during the task template resolution.
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
&self.substituted_variables

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{net::Ipv4Addr, path::PathBuf};
use anyhow::{bail, Context};
use collections::{HashMap, HashSet};
@@ -51,6 +51,14 @@ pub struct TaskTemplate {
/// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`.
#[serde(default)]
pub hide: HideStrategy,
/// If this task should start a debugger or not
#[serde(default)]
pub task_type: TaskType,
/// Specific configuration for the debug adapter
/// This is only used if `task_type` is `Debug`
#[serde(default)]
pub debug_adapter: Option<DebugAdapterConfig>,
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
#[serde(default)]
pub tags: Vec<String>,
@@ -59,6 +67,80 @@ pub struct TaskTemplate {
pub shell: Shell,
}
/// Represents the type of task that is being ran
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TaskType {
/// Act like a typically task that runs commands
#[default]
Script,
/// This task starts the debugger for a language
Debug,
}
/// Represents the type of the debugger adapter connection
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "lowercase", tag = "connection")]
pub enum DebugConnectionType {
/// Connect to the debug adapter via TCP
TCP(TCPHost),
/// Connect to the debug adapter via STDIO
STDIO,
}
impl Default for DebugConnectionType {
fn default() -> Self {
DebugConnectionType::TCP(TCPHost::default())
}
}
/// Represents the host information of the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
pub struct TCPHost {
/// The port that the debug adapter is listening on
pub port: Option<u16>,
/// The host that the debug adapter is listening too
pub host: Option<Ipv4Addr>,
/// The delay in ms between starting and connecting to the debug adapter
pub delay: Option<u64>,
}
/// Represents the type that will determine which request to call on the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum DebugRequestType {
/// Call the `launch` request on the debug adapter
#[default]
Launch,
/// Call the `attach` request on the debug adapter
Attach,
}
/// Represents the configuration for the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub struct DebugAdapterConfig {
/// Unique id of for the debug adapter,
/// that will be send with the `initialize` request
pub id: String,
/// The type of connection the adapter should use
#[serde(default, flatten)]
pub connection: DebugConnectionType,
/// The type of request that should be called on the debug adapter
#[serde(default)]
pub request: DebugRequestType,
/// The configuration options that are send with the `launch` or `attach` request
/// to the debug adapter
pub request_args: Option<DebugRequestArgs>,
}
/// Represents the configuration for the debug adapter that is send with the launch request
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(transparent)]
pub struct DebugRequestArgs {
pub args: serde_json::Value,
}
/// What to do with the terminal pane and tab, after the command was started.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]

View File

@@ -3,6 +3,7 @@ use editor::{tasks::task_context, Editor};
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
use modal::TasksModal;
use project::{Location, WorktreeId};
use task::TaskType;
use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace};
@@ -70,7 +71,7 @@ pub fn init(cx: &mut AppContext) {
);
}
} else {
toggle_modal(workspace, cx).detach();
toggle_modal(workspace, cx, TaskType::Script).detach();
};
});
},
@@ -81,11 +82,15 @@ pub fn init(cx: &mut AppContext) {
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match &action.task_name {
Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
None => toggle_modal(workspace, cx).detach(),
None => toggle_modal(workspace, cx, task::TaskType::Script).detach(),
}
}
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
pub fn toggle_modal(
workspace: &mut Workspace,
cx: &mut ViewContext<'_, Workspace>,
task_type: TaskType,
) -> AsyncTask<()> {
let project = workspace.project().clone();
let workspace_handle = workspace.weak_handle();
let context_task = task_context(workspace, cx);
@@ -97,7 +102,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
project.is_local() || project.ssh_connection_string(cx).is_some()
}) {
workspace.toggle_modal(cx, |cx| {
TasksModal::new(project, task_context, workspace_handle, cx)
TasksModal::new(project, task_context, workspace_handle, cx, task_type)
})
}
})

View File

@@ -9,7 +9,7 @@ use gpui::{
};
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
use project::{Project, TaskSourceKind};
use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate};
use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskType};
use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
@@ -73,6 +73,8 @@ pub(crate) struct TasksModalDelegate {
prompt: String,
task_context: TaskContext,
placeholder_text: Arc<str>,
/// If this delegate is responsible for running a scripting task or a debugger
task_type: TaskType,
}
impl TasksModalDelegate {
@@ -80,6 +82,7 @@ impl TasksModalDelegate {
project: Model<Project>,
task_context: TaskContext,
workspace: WeakView<Workspace>,
task_type: TaskType,
) -> Self {
Self {
project,
@@ -92,6 +95,7 @@ impl TasksModalDelegate {
prompt: String::default(),
task_context,
placeholder_text: Arc::from("Find a task, or run a command"),
task_type,
}
}
@@ -143,10 +147,11 @@ impl TasksModal {
task_context: TaskContext,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
task_type: TaskType,
) -> Self {
let picker = cx.new_view(|cx| {
Picker::uniform_list(
TasksModalDelegate::new(project, task_context, workspace),
TasksModalDelegate::new(project, task_context, workspace, task_type),
cx,
)
});
@@ -203,12 +208,13 @@ impl PickerDelegate for TasksModalDelegate {
query: String,
cx: &mut ViewContext<picker::Picker<Self>>,
) -> Task<()> {
let task_type = self.task_type.clone();
cx.spawn(move |picker, mut cx| async move {
let Some(candidates_task) = picker
.update(&mut cx, |picker, cx| {
match &mut picker.delegate.candidates {
Some(candidates) => {
Task::ready(Ok(string_match_candidates(candidates.iter())))
Task::ready(Ok(string_match_candidates(candidates.iter(), task_type)))
}
None => {
let Ok((worktree, location)) =
@@ -252,6 +258,7 @@ impl PickerDelegate for TasksModalDelegate {
)
}
});
cx.spawn(|picker, mut cx| async move {
let (used, current) = resolved_task.await;
picker.update(&mut cx, |picker, _| {
@@ -264,7 +271,7 @@ impl PickerDelegate for TasksModalDelegate {
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates =
string_match_candidates(new_candidates.iter());
string_match_candidates(new_candidates.iter(), task_type);
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
})
@@ -334,7 +341,20 @@ impl PickerDelegate for TasksModalDelegate {
self.workspace
.update(cx, |workspace, cx| {
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
match task.task_type() {
TaskType::Script => schedule_resolved_task(
workspace,
task_source_kind,
task,
omit_history_entry,
cx,
),
// TODO: Should create a schedule_resolved_debug_task function
// This would allow users to access to debug history and other issues
TaskType::Debug => workspace.project().update(cx, |project, cx| {
project.start_debug_adapter_client(task, cx)
}),
};
})
.ok();
cx.emit(DismissEvent);
@@ -473,9 +493,23 @@ impl PickerDelegate for TasksModalDelegate {
let Some((task_source_kind, task)) = self.spawn_oneshot() else {
return;
};
self.workspace
.update(cx, |workspace, cx| {
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
match task.task_type() {
TaskType::Script => schedule_resolved_task(
workspace,
task_source_kind,
task,
omit_history_entry,
cx,
),
// TODO: Should create a schedule_resolved_debug_task function
// This would allow users to access to debug history and other issues
TaskType::Debug => workspace.project().update(cx, |project, cx| {
project.start_debug_adapter_client(task, cx)
}),
};
})
.ok();
cx.emit(DismissEvent);
@@ -582,9 +616,11 @@ impl PickerDelegate for TasksModalDelegate {
fn string_match_candidates<'a>(
candidates: impl Iterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
task_type: TaskType,
) -> Vec<StringMatchCandidate> {
candidates
.enumerate()
.filter(|(_, (_, candidate))| candidate.task_type() == task_type)
.map(|(index, (_, candidate))| StringMatchCandidate {
id: index,
char_bag: candidate.resolved_label.chars().collect(),

View File

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

View File

@@ -71,6 +71,7 @@ impl ThemeColors {
editor_subheader_background: neutral().light().step_2(),
editor_active_line_background: neutral().light_alpha().step_3(),
editor_highlighted_line_background: neutral().light_alpha().step_3(),
editor_debugger_active_line_background: neutral().light().step_8(),
editor_line_number: neutral().light().step_10(),
editor_active_line_number: neutral().light().step_11(),
editor_invisible: neutral().light().step_10(),
@@ -169,6 +170,7 @@ impl ThemeColors {
editor_subheader_background: neutral().dark().step_3(),
editor_active_line_background: neutral().dark_alpha().step_3(),
editor_highlighted_line_background: neutral().dark_alpha().step_4(),
editor_debugger_active_line_background: neutral().light_alpha().step_5(),
editor_line_number: neutral().dark_alpha().step_10(),
editor_active_line_number: neutral().dark_alpha().step_12(),
editor_invisible: neutral().dark_alpha().step_4(),

View File

@@ -88,6 +88,12 @@ pub(crate) fn one_dark() -> Theme {
editor_subheader_background: bg,
editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0),
editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1),
editor_debugger_active_line_background: hsla(
207.8 / 360.,
81. / 100.,
66. / 100.,
0.2,
),
editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0),
editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),

View File

@@ -375,6 +375,10 @@ pub struct ThemeColorsContent {
#[serde(rename = "editor.highlighted_line.background")]
pub editor_highlighted_line_background: Option<String>,
/// Background of active line of debugger
#[serde(rename = "editor.debugger_active_line.background")]
pub editor_debugger_active_line_background: Option<String>,
/// Text Color. Used for the text of the line number in the editor gutter.
#[serde(rename = "editor.line_number")]
pub editor_line_number: Option<String>,
@@ -756,6 +760,10 @@ impl ThemeColorsContent {
.editor_highlighted_line_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
editor_debugger_active_line_background: self
.editor_debugger_active_line_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
editor_line_number: self
.editor_line_number
.as_ref()

View File

@@ -147,6 +147,8 @@ pub struct ThemeColors {
pub editor_subheader_background: Hsla,
pub editor_active_line_background: Hsla,
pub editor_highlighted_line_background: Hsla,
/// Line color of the line a debugger is currently stopped at
pub editor_debugger_active_line_background: Hsla,
/// Text Color. Used for the text of the line number in the editor gutter.
pub editor_line_number: Hsla,
/// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted.

View File

@@ -148,6 +148,13 @@ pub enum IconName {
Copy,
CountdownTimer,
Dash,
DebugPause,
DebugContinue,
DebugStepOver,
DebugStepInto,
DebugStepOut,
DebugRestart,
DebugStop,
Delete,
Disconnected,
Download,
@@ -299,6 +306,13 @@ impl IconName {
IconName::Copy => "icons/copy.svg",
IconName::CountdownTimer => "icons/countdown_timer.svg",
IconName::Dash => "icons/dash.svg",
IconName::DebugPause => "icons/debug-pause.svg",
IconName::DebugContinue => "icons/debug-continue.svg",
IconName::DebugStepOver => "icons/debug-step-over.svg",
IconName::DebugStepInto => "icons/debug-step-into.svg",
IconName::DebugStepOut => "icons/debug-step-out.svg",
IconName::DebugRestart => "icons/debug-restart.svg",
IconName::DebugStop => "icons/debug-stop.svg",
IconName::Delete => "icons/delete.svg",
IconName::Disconnected => "icons/disconnected.svg",
IconName::Download => "icons/download.svg",

View File

@@ -21,6 +21,7 @@ use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
println!("TEST");
let mut cx = VimTestContext::new(cx, false).await;
cx.simulate_keystrokes("h j k l");
cx.assert_editor_state("hjklˇ");

View File

@@ -242,7 +242,7 @@ pub struct Pane {
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
/// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
display_nav_history_buttons: Option<bool>,
double_click_dispatch_action: Box<dyn Action>,
double_click_dispatch_action: Option<Box<dyn Action>>,
save_modals_spawned: HashSet<EntityId>,
}
@@ -310,7 +310,7 @@ impl Pane {
project: Model<Project>,
next_timestamp: Arc<AtomicUsize>,
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
double_click_dispatch_action: Box<dyn Action>,
double_click_dispatch_action: Option<Box<dyn Action>>,
cx: &mut ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
@@ -1894,7 +1894,9 @@ impl Pane {
}))
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
if let Some(action) = &this.double_click_dispatch_action {
cx.dispatch_action(action.boxed_clone());
}
}
})),
)

View File

@@ -136,6 +136,7 @@ actions!(
ReloadActiveItem,
SaveAs,
SaveWithoutFormat,
StartDebugger,
ToggleBottomDock,
ToggleCenteredLayout,
ToggleLeftDock,
@@ -855,7 +856,7 @@ impl Workspace {
project.clone(),
pane_history_timestamp.clone(),
None,
NewFile.boxed_clone(),
Some(NewFile.boxed_clone()),
cx,
)
});
@@ -2368,7 +2369,7 @@ impl Workspace {
self.project.clone(),
self.pane_history_timestamp.clone(),
None,
NewFile.boxed_clone(),
Some(NewFile.boxed_clone()),
cx,
)
});

View File

@@ -33,6 +33,7 @@ collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true
copilot.workspace = true
debugger_ui.workspace = true
db.workspace = true
diagnostics.workspace = true
editor.workspace = true

View File

@@ -428,6 +428,7 @@ fn main() {
zed::init(cx);
project::Project::init(&client, cx);
debugger_ui::init(cx);
client::init(&client, cx);
language::init(cx);
let telemetry = client.telemetry();

View File

@@ -12,6 +12,7 @@ pub use app_menus::*;
use breadcrumbs::Breadcrumbs;
use client::ZED_URL_SCHEME;
use collections::VecDeque;
use debugger_ui::debugger_panel::DebugPanel;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
@@ -254,6 +255,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace_handle.clone(),
cx.clone(),
);
let debug_panel = DebugPanel::load(workspace_handle.clone(), cx.clone());
let (
project_panel,
@@ -263,6 +265,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
channels_panel,
chat_panel,
notification_panel,
debug_panel,
) = futures::try_join!(
project_panel,
outline_panel,
@@ -271,6 +274,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
channels_panel,
chat_panel,
notification_panel,
debug_panel
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
@@ -281,6 +285,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.add_panel(channels_panel, cx);
workspace.add_panel(chat_panel, cx);
workspace.add_panel(notification_panel, cx);
workspace.add_panel(debug_panel, cx);
cx.focus_self();
})
})
@@ -3462,6 +3467,7 @@ mod tests {
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
repl::init(app_state.fs.clone(), cx);
tasks_ui::init(cx);
debugger_ui::init(cx);
initialize_workspace(app_state.clone(), cx);
app_state
})