History

Debugger implementation (#13433)

### DISCLAIMER > As of 6th March 2025, debugger is still in development. We plan to merge it behind a staff-only feature flag for staff use only, followed by non-public release and then finally a public one (akin to how Git panel release was handled). This is done to ensure the best experience when it gets released. ### END OF DISCLAIMER **The current state of the debugger implementation:** https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9 https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f ---- All the todo's are in the following channel, so it's easier to work on this together: https://zed.dev/channel/zed-debugger-11370 If you are on Linux, you can use the following command to join the channel: ```cli zed https://zed.dev/channel/zed-debugger-11370 ``` ## Current Features - Collab - Breakpoints - Sync when you (re)join a project - Sync when you add/remove a breakpoint - Sync active debug line - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Restart stack frame (if adapter supports this) - Variables - Loaded sources - Modules - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Debug console - Breakpoints - Log breakpoints - line breakpoints - Persistent between zed sessions (configurable) - Multi buffer support - Toggle disable/enable all breakpoints - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Show collapsed stack frames - Restart stack frame (if adapter supports this) - Loaded sources - View all used loaded sources if supported by adapter. - Modules - View all used modules (if adapter supports this) - Variables - Copy value - Copy name - Copy memory reference - Set value (if adapter supports this) - keyboard navigation - Debug Console - See logs - View output that was sent from debug adapter - Output grouping - Evaluate code - Updates the variable list - Auto completion - If not supported by adapter, we will show auto-completion for existing variables - Debug Terminal - Run custom commands and change env values right inside your Zed terminal - Attach to process (if adapter supports this) - Process picker - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Disconnect - Restart - Stop - Warning when a debug session exited without hitting any breakpoint - Debug view to see Adapter/RPC log messages - Testing - Fake debug adapter - Fake requests & events --- Release Notes: - N/A ---------

Remco Smits committed 1 year ago

Commit 41a60ff1 parent ed4e654156 files · +25837 −448Browse files

Unified · Split

156 files
  1. .zed/debug.json +19 −0
  2. Cargo.lock +134 −1
  3. Cargo.toml +10 −0
  4. assets/icons/debug.svg +1 −0
  5. assets/icons/debug_breakpoint.svg +1 −0
  6. assets/icons/debug_continue.svg +1 −0
  7. assets/icons/debug_disconnect.svg +1 −0
  8. assets/icons/debug_ignore_breakpoints.svg +1 −0
  9. assets/icons/debug_log_breakpoint.svg +1 −0
  10. assets/icons/debug_pause.svg +1 −0
  11. assets/icons/debug_restart.svg +1 −0
  12. assets/icons/debug_step_back.svg +1 −0
  13. assets/icons/debug_step_into.svg +5 −0
  14. assets/icons/debug_step_out.svg +5 −0
  15. assets/icons/debug_step_over.svg +5 −0
  16. assets/icons/debug_stop.svg +1 −0
  17. assets/keymaps/default-linux.json +10 −1
  18. assets/keymaps/default-macos.json +17 −0
  19. assets/keymaps/linux/jetbrains.json +11 −2
  20. assets/keymaps/macos/jetbrains.json +11 −2
  21. assets/settings/default.json +9 −1
  22. assets/settings/initial_debug_tasks.json +32 −0
  23. crates/activity_indicator/Cargo.toml +0 −1
  24. crates/activity_indicator/src/activity_indicator.rs +32 −22
  25. crates/assistant_context_editor/src/context_editor.rs +1 −0
  26. crates/collab/Cargo.toml +3 −0
  27. crates/collab/migrations.sqlite/20221109000000_test_schema.sql +11 −0
  28. crates/collab/migrations/20241121185750_add_breakpoints.sql +11 −0
  29. crates/collab/src/db/queries/projects.rs +40 −29
  30. crates/collab/src/rpc.rs +3 −1
  31. crates/collab/src/tests.rs +1 −0
  32. crates/collab/src/tests/debug_panel_tests.rs +2454 −0
  33. crates/collab/src/tests/editor_tests.rs +204 −1
  34. crates/dap/Cargo.toml +53 −0
  35. crates/dap/LICENSE-GPL +1 −0
  36. crates/dap/docs/breakpoints.md +9 −0
  37. crates/dap/src/adapters.rs +370 −0
  38. crates/dap/src/client.rs +490 −0
  39. crates/dap/src/debugger_settings.rs +59 −0
  40. crates/dap/src/lib.rs +38 −0
  41. crates/dap/src/proto_conversions.rs +591 −0
  42. crates/dap/src/transport.rs +891 −0
  43. crates/dap_adapters/Cargo.toml +41 −0
  44. crates/dap_adapters/LICENSE-GPL +1 −0
  45. crates/dap_adapters/src/custom.rs +84 −0
  46. crates/dap_adapters/src/dap_adapters.rs +67 −0
  47. crates/dap_adapters/src/gdb.rs +83 −0
  48. crates/dap_adapters/src/go.rs +100 −0
  49. crates/dap_adapters/src/javascript.rs +148 −0
  50. crates/dap_adapters/src/lldb.rs +104 −0
  51. crates/dap_adapters/src/php.rs +123 −0
  52. crates/dap_adapters/src/python.rs +142 −0
  53. crates/debugger_tools/Cargo.toml +26 −0
  54. crates/debugger_tools/LICENSE-GPL +1 −0
  55. crates/debugger_tools/src/dap_log.rs +845 −0
  56. crates/debugger_tools/src/debugger_tools.rs +8 −0
  57. crates/debugger_ui/Cargo.toml +58 −0
  58. crates/debugger_ui/LICENSE-GPL +1 −0
  59. crates/debugger_ui/src/attach_modal.rs +293 −0
  60. crates/debugger_ui/src/debugger_panel.rs +536 −0
  61. crates/debugger_ui/src/lib.rs +122 −0
  62. crates/debugger_ui/src/session.rs +313 −0
  63. crates/debugger_ui/src/session/failed.rs +30 −0
  64. crates/debugger_ui/src/session/inert.rs +219 −0
  65. crates/debugger_ui/src/session/running.rs +686 −0
  66. crates/debugger_ui/src/session/running/console.rs +419 −0
  67. crates/debugger_ui/src/session/running/loaded_source_list.rs +103 −0
  68. crates/debugger_ui/src/session/running/module_list.rs +183 −0
  69. crates/debugger_ui/src/session/running/stack_frame_list.rs +519 −0
  70. crates/debugger_ui/src/session/running/variable_list.rs +946 −0
  71. crates/debugger_ui/src/session/starting.rs +80 −0
  72. crates/debugger_ui/src/tests.rs +75 −0
  73. crates/debugger_ui/src/tests/attach_modal.rs +136 −0
  74. crates/debugger_ui/src/tests/console.rs +916 −0
  75. crates/debugger_ui/src/tests/debugger_panel.rs +1084 −0
  76. crates/debugger_ui/src/tests/module_list.rs +262 −0
  77. crates/debugger_ui/src/tests/stack_frame_list.rs +845 −0
  78. crates/debugger_ui/src/tests/variable_list.rs +1759 −0
  79. crates/editor/Cargo.toml +2 −0
  80. crates/editor/src/actions.rs +2 −0
  81. crates/editor/src/editor.rs +761 −7
  82. crates/editor/src/editor_settings.rs +5 −0
  83. crates/editor/src/editor_tests.rs +337 −2
  84. crates/editor/src/element.rs +199 −41
  85. crates/extension/src/extension_host_proxy.rs +3 −3
  86. crates/extension_host/src/extension_store_test.rs +5 −14
  87. crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +5 −11
  88. crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +5 −11
  89. crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs +5 −11
  90. crates/feature_flags/src/feature_flags.rs +6 −1
  91. crates/language/src/buffer_tests.rs +1 −1
  92. crates/language/src/language.rs +8 −8
  93. crates/language/src/language_registry.rs +22 −16
  94. crates/language_extension/src/extension_lsp_adapter.rs +3 −3
  95. crates/languages/src/json.rs +10 −1
  96. crates/paths/src/paths.rs +24 −0
  97. crates/project/Cargo.toml +10 −2
  98. crates/project/src/buffer_store.rs +31 −4
  99. crates/project/src/debugger.rs +19 −0
  100. crates/project/src/debugger/README.md +350 −0
  101. crates/project/src/debugger/breakpoint_store.rs +612 −0
  102. crates/project/src/debugger/dap_command.rs +1738 −0
  103. crates/project/src/debugger/dap_store.rs +882 −0
  104. crates/project/src/debugger/session.rs +1762 −0
  105. crates/project/src/lsp_store.rs +7 −11
  106. crates/project/src/project.rs +180 −14
  107. crates/project/src/project_settings.rs +32 −8
  108. crates/project/src/project_tests.rs +1 −0
  109. crates/project/src/task_inventory.rs +29 −11
  110. crates/project/src/task_store.rs +45 −12
  111. crates/project/src/terminals.rs +33 −0
  112. crates/proto/build.rs +3 −0
  113. crates/proto/proto/zed.proto +537 −4
  114. crates/proto/src/proto.rs +77 −71
  115. crates/recent_projects/Cargo.toml +2 −1
  116. crates/recent_projects/src/recent_projects.rs +2 −0
  117. crates/remote_server/src/headless_project.rs +34 −9
  118. crates/settings/src/settings.rs +5 −1
  119. crates/settings/src/settings_store.rs +8 −2
  120. crates/sqlez/src/bindable.rs +2 −0
  121. crates/sqlez/src/statement.rs +6 −0
  122. crates/task/Cargo.toml +8 −0
  123. crates/task/src/debug_format.rs +227 −0
  124. crates/task/src/lib.rs +32 −1
  125. crates/task/src/task_template.rs +91 −3
  126. crates/tasks_ui/Cargo.toml +2 −0
  127. crates/tasks_ui/src/modal.rs +78 −7
  128. crates/tasks_ui/src/tasks_ui.rs +27 −8
  129. crates/terminal/src/pty_info.rs +4 −0
  130. crates/terminal/src/terminal.rs +7 −0
  131. crates/terminal_view/src/terminal_panel.rs +1 −1
  132. crates/terminal_view/src/terminal_view.rs +4 −1
  133. crates/text/src/text.rs +1 −1
  134. crates/theme/src/default_colors.rs +5 −1
  135. crates/theme/src/fallback_themes.rs +7 −0
  136. crates/theme/src/schema.rs +19 −2
  137. crates/theme/src/styles/colors.rs +7 −2
  138. crates/ui/src/components/button/button_like.rs +42 −1
  139. crates/ui/src/components/button/icon_button.rs +9 −0
  140. crates/ui/src/components/dropdown_menu.rs +5 −1
  141. crates/ui/src/components/icon.rs +13 −0
  142. crates/ui/src/components/list/list_item.rs +10 −1
  143. crates/ui/src/styles/color.rs +3 −0
  144. crates/ui/src/utils/search_input.rs +1 −1
  145. crates/util/src/fs.rs +67 −2
  146. crates/vim/src/command.rs +1 −0
  147. crates/workspace/src/pane.rs +19 −9
  148. crates/workspace/src/persistence.rs +309 −2
  149. crates/workspace/src/persistence/model.rs +3 −1
  150. crates/workspace/src/workspace.rs +38 −2
  151. crates/zed/Cargo.toml +2 −0
  152. crates/zed/src/main.rs +2 −0
  153. crates/zed/src/zed.rs +82 −58
  154. crates/zeta/src/rate_completion_modal.rs +1 −0
  155. docs/src/languages/markdown.md +1 −1
  156. docs/src/model-improvement.md +1 −1
.zed/debug.json+19−0
@@ -0,0 +1,19 @@
1+[
2+ {
3+ "label": "Debug Zed with LLDB",
4+ "adapter": "lldb",
5+ "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
6+ "request": "launch",
7+ "cwd": "$ZED_WORKTREE_ROOT"
8+ },
9+ {
10+ "label": "Debug Zed with GDB",
11+ "adapter": "gdb",
12+ "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
13+ "request": "launch",
14+ "cwd": "$ZED_WORKTREE_ROOT",
15+ "initialize_args": {
16+ "stopAtBeginningOfMainSubprogram": true
17+ }
18+ }
19+]
Cargo.lock+134−1
@@ -13,7 +13,6 @@ dependencies = [ · 12 unmodified lines
1313 "futures 0.3.31",
1414 "gpui",
1515 "language",
16− "lsp",
1716 "project",
1817 "smallvec",
1918 "ui",
@@ -2644,6 +2643,12 @@ dependencies = [ · 2624 unmodified lines
26442643 "zeroize",
26452644 ]
26462645
2646+[[package]]
2647+name = "circular-buffer"
2648+version = "1.0.0"
2649+source = "registry+https://github.com/rust-lang/crates.io-index"
2650+checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f"
2651+
26472652 [[package]]
26482653 name = "clang-sys"
26492654 version = "1.8.1"
@@ -2893,9 +2898,12 @@ dependencies = [ · 243 unmodified lines
28932898 "clock",
28942899 "collab_ui",
28952900 "collections",
2901+ "command_palette_hooks",
28962902 "context_server",
28972903 "ctor",
2904+ "dap",
28982905 "dashmap 6.1.0",
2906+ "debugger_ui",
28992907 "derive_more",
29002908 "editor",
29012909 "env_logger 0.11.7",
@@ -3839,6 +3847,66 @@ dependencies = [ · 937 unmodified lines
38393847 "syn 2.0.100",
38403848 ]
38413849
3850+[[package]]
3851+name = "dap"
3852+version = "0.1.0"
3853+dependencies = [
3854+ "anyhow",
3855+ "async-compression",
3856+ "async-pipe",
3857+ "async-tar",
3858+ "async-trait",
3859+ "client",
3860+ "collections",
3861+ "dap-types",
3862+ "env_logger 0.11.7",
3863+ "fs",
3864+ "futures 0.3.31",
3865+ "gpui",
3866+ "http_client",
3867+ "language",
3868+ "log",
3869+ "node_runtime",
3870+ "parking_lot",
3871+ "paths",
3872+ "schemars",
3873+ "serde",
3874+ "serde_json",
3875+ "settings",
3876+ "smallvec",
3877+ "smol",
3878+ "task",
3879+ "util",
3880+]
3881+
3882+[[package]]
3883+name = "dap-types"
3884+version = "0.0.1"
3885+source = "git+https://github.com/zed-industries/dap-types?rev=bfd4af0#bfd4af084bbaa5f344e6925370d7642e41d0b5b8"
3886+dependencies = [
3887+ "schemars",
3888+ "serde",
3889+ "serde_json",
3890+]
3891+
3892+[[package]]
3893+name = "dap_adapters"
3894+version = "0.1.0"
3895+dependencies = [
3896+ "anyhow",
3897+ "async-trait",
3898+ "dap",
3899+ "gpui",
3900+ "language",
3901+ "paths",
3902+ "regex",
3903+ "serde",
3904+ "serde_json",
3905+ "sysinfo",
3906+ "task",
3907+ "util",
3908+]
3909+
38423910 [[package]]
38433911 name = "dashmap"
38443912 version = "5.5.3"
@@ -3912,6 +3980,58 @@ dependencies = [ · 67 unmodified lines
39123980 "winapi",
39133981 ]
39143982
3983+[[package]]
3984+name = "debugger_tools"
3985+version = "0.1.0"
3986+dependencies = [
3987+ "anyhow",
3988+ "dap",
3989+ "editor",
3990+ "futures 0.3.31",
3991+ "gpui",
3992+ "project",
3993+ "serde_json",
3994+ "settings",
3995+ "smol",
3996+ "util",
3997+ "workspace",
3998+]
3999+
4000+[[package]]
4001+name = "debugger_ui"
4002+version = "0.1.0"
4003+dependencies = [
4004+ "anyhow",
4005+ "client",
4006+ "collections",
4007+ "command_palette_hooks",
4008+ "dap",
4009+ "editor",
4010+ "env_logger 0.11.7",
4011+ "feature_flags",
4012+ "futures 0.3.31",
4013+ "fuzzy",
4014+ "gpui",
4015+ "language",
4016+ "log",
4017+ "menu",
4018+ "picker",
4019+ "pretty_assertions",
4020+ "project",
4021+ "rpc",
4022+ "serde",
4023+ "serde_json",
4024+ "settings",
4025+ "sysinfo",
4026+ "task",
4027+ "terminal_view",
4028+ "theme",
4029+ "ui",
4030+ "unindent",
4031+ "util",
4032+ "workspace",
4033+]
4034+
39154035 [[package]]
39164036 name = "deepseek"
39174037 version = "0.1.0"
@@ -4212,6 +4332,7 @@ dependencies = [ · 294 unmodified lines
42124332 "db",
42134333 "emojis",
42144334 "env_logger 0.11.7",
4335+ "feature_flags",
42154336 "file_icons",
42164337 "fs",
42174338 "futures 0.3.31",
@@ -4228,6 +4349,7 @@ dependencies = [ · 10 unmodified lines
42284349 "log",
42294350 "lsp",
42304351 "markdown",
4352+ "menu",
42314353 "multi_buffer",
42324354 "ordered-float 2.10.1",
42334355 "parking_lot",
@@ -10422,9 +10544,12 @@ dependencies = [ · 6188 unmodified lines
1042210544 "askpass",
1042310545 "async-trait",
1042410546 "buffer_diff",
10547+ "circular-buffer",
1042510548 "client",
1042610549 "clock",
1042710550 "collections",
10551+ "dap",
10552+ "dap_adapters",
1042810553 "env_logger 0.11.7",
1042910554 "extension",
1043010555 "fancy-regex 0.14.0",
@@ -10437,6 +10562,7 @@ dependencies = [ · 6 unmodified lines
1043710562 "gpui",
1043810563 "http_client",
1043910564 "image",
10565+ "indexmap",
1044010566 "itertools 0.14.0",
1044110567 "language",
1044210568 "log",
@@ -11100,6 +11226,7 @@ version = "0.1.0" · 657 unmodified lines
1110011226 dependencies = [
1110111227 "anyhow",
1110211228 "auto_update",
11229+ "dap",
1110311230 "editor",
1110411231 "extension_host",
1110511232 "file_finder",
@@ -13657,12 +13784,14 @@ version = "0.1.0" · 2551 unmodified lines
1365713784 dependencies = [
1365813785 "anyhow",
1365913786 "collections",
13787+ "dap-types",
1366013788 "futures 0.3.31",
1366113789 "gpui",
1366213790 "hex",
1366313791 "parking_lot",
1366413792 "schemars",
1366513793 "serde",
13794+ "serde_json",
1366613795 "serde_json_lenient",
1366713796 "sha2",
1366813797 "shellexpand 2.1.2",
@@ -13675,7 +13804,9 @@ name = "tasks_ui" · 6 unmodified lines
1367513804 version = "0.1.0"
1367613805 dependencies = [
1367713806 "anyhow",
13807+ "debugger_ui",
1367813808 "editor",
13809+ "feature_flags",
1367913810 "file_icons",
1368013811 "fuzzy",
1368113812 "gpui",
@@ -17218,6 +17349,8 @@ dependencies = [ · 3536 unmodified lines
1721817349 "component_preview",
1721917350 "copilot",
1722017351 "db",
17352+ "debugger_tools",
17353+ "debugger_ui",
1722117354 "diagnostics",
1722217355 "editor",
1722317356 "env_logger 0.11.7",
Cargo.toml+10−0
@@ -37,6 +37,10 @@ members = [ · 36 unmodified lines
3737 "crates/context_server_settings",
3838 "crates/copilot",
3939 "crates/credentials_provider",
40+ "crates/dap",
41+ "crates/dap_adapters",
42+ "crates/debugger_tools",
43+ "crates/debugger_ui",
4044 "crates/db",
4145 "crates/deepseek",
4246 "crates/diagnostics",
@@ -236,7 +240,11 @@ context_server = { path = "crates/context_server" } · 193 unmodified lines
236240 context_server_settings = { path = "crates/context_server_settings" }
237241 copilot = { path = "crates/copilot" }
238242 credentials_provider = { path = "crates/credentials_provider" }
243+dap = { path = "crates/dap" }
244+dap_adapters = { path = "crates/dap_adapters" }
239245 db = { path = "crates/db" }
246+debugger_ui = { path = "crates/debugger_ui" }
247+debugger_tools = { path = "crates/debugger_tools" }
240248 deepseek = { path = "crates/deepseek" }
241249 diagnostics = { path = "crates/diagnostics" }
242250 buffer_diff = { path = "crates/buffer_diff" }
@@ -402,6 +410,7 @@ bytes = "1.0" · 159 unmodified lines
402410 cargo_metadata = "0.19"
403411 cargo_toml = "0.21"
404412 chrono = { version = "0.4", features = ["serde"] }
413+circular-buffer = "1.0"
405414 clap = { version = "4.4", features = ["derive"] }
406415 cocoa = "0.26"
407416 cocoa-foundation = "0.2.0"
@@ -410,6 +419,7 @@ core-foundation = "0.9.3" · 2 unmodified lines
410419 core-foundation-sys = "0.8.6"
411420 ctor = "0.4.0"
412421 dashmap = "6.0"
422+dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" }
413423 derive_more = "0.99.17"
414424 dirs = "4.0"
415425 ec4rs = "1.1"
assets/icons/debug.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
assets/icons/debug_breakpoint.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
assets/icons/debug_continue.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-step-forward"><line x1="6" x2="6" y1="4" y2="20"/><polygon points="10,4 20,12 10,20"/></svg>
assets/icons/debug_disconnect.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unplug"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg>
assets/icons/debug_ignore_breakpoints.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>
assets/icons/debug_log_breakpoint.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
assets/icons/debug_pause.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause"><rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/></svg>
assets/icons/debug_restart.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
assets/icons/debug_step_back.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo-dot"><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/><path d="M3 7v6h6"/><circle cx="12" cy="17" r="1"/></svg>
assets/icons/debug_step_into.svg+5−0
@@ -0,0 +1,5 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
2+ <path d="m5 15 7 7 7-7"/>
3+ <path d="M12 8v14"/>
4+ <circle cx="12" cy="3" r="1"/>
5+</svg>
assets/icons/debug_step_out.svg+5−0
@@ -0,0 +1,5 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
2+ <path d="m3 10 9-8 9 8"/>
3+ <path d="M12 17V2"/>
4+ <circle cx="12" cy="21" r="1"/>
5+</svg>
assets/icons/debug_step_over.svg+5−0
@@ -0,0 +1,5 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-redo-dot">
2+ <circle cx="12" cy="17" r="1"/>
3+ <path d="M21 7v6h-6"/>
4+ <path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/>
5+</svg>
assets/icons/debug_stop.svg+1−0
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>
assets/keymaps/default-linux.json+10−1
@@ -30,6 +30,13 @@ · 29 unmodified lines
3030 "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
3131 "ctrl-,": "zed::OpenSettings",
3232 "ctrl-q": "zed::Quit",
33+ "f4": "debugger::Start",
34+ "f5": "debugger::Continue",
35+ "shift-f5": "debugger::Stop",
36+ "f6": "debugger::Pause",
37+ "f7": "debugger::StepOver",
38+ "cmd-f11": "debugger::StepInto",
39+ "shift-f11": "debugger::StepOut",
3340 "f11": "zed::ToggleFullScreen",
3441 "ctrl-alt-z": "edit_prediction::RateCompletions",
3542 "ctrl-shift-i": "edit_prediction::ToggleMenu"
@@ -124,7 +131,9 @@ · 88 unmodified lines
124131 "alt-g b": "editor::ToggleGitBlame",
125132 "menu": "editor::OpenContextMenu",
126133 "shift-f10": "editor::OpenContextMenu",
127− "ctrl-shift-e": "editor::ToggleEditPrediction"
134+ "ctrl-shift-e": "editor::ToggleEditPrediction",
135+ "f9": "editor::ToggleBreakpoint",
136+ "shift-f9": "editor::EditLogBreakpoint"
128137 }
129138 },
130139 {
assets/keymaps/default-macos.json+17−0
@@ -14,6 +14,13 @@ · 13 unmodified lines
1414 {
1515 "use_key_equivalents": true,
1616 "bindings": {
17+ "f4": "debugger::Start",
18+ "f5": "debugger::Continue",
19+ "shift-f5": "debugger::Stop",
20+ "f6": "debugger::Pause",
21+ "f7": "debugger::StepOver",
22+ "f11": "debugger::StepInto",
23+ "shift-f11": "debugger::StepOut",
1724 "home": "menu::SelectFirst",
1825 "shift-pageup": "menu::SelectFirst",
1926 "pageup": "menu::SelectFirst",
@@ -148,6 +155,8 @@ · 128 unmodified lines
148155 "cmd-\"": "editor::ExpandAllDiffHunks",
149156 "cmd-alt-g b": "editor::ToggleGitBlame",
150157 "cmd-i": "editor::ShowSignatureHelp",
158+ "f9": "editor::ToggleBreakpoint",
159+ "shift-f9": "editor::EditLogBreakpoint",
151160 "ctrl-f12": "editor::GoToDeclaration",
152161 "alt-ctrl-f12": "editor::GoToDeclarationSplit",
153162 "ctrl-cmd-e": "editor::ToggleEditPrediction"
@@ -756,6 +765,14 @@ · 602 unmodified lines
756765 "space": "project_panel::Open"
757766 }
758767 },
768+ {
769+ "context": "VariableList",
770+ "use_key_equivalents": true,
771+ "bindings": {
772+ "left": "variable_list::CollapseSelectedEntry",
773+ "right": "variable_list::ExpandSelectedEntry"
774+ }
775+ },
759776 {
760777 "context": "GitPanel && ChangesList",
761778 "use_key_equivalents": true,
assets/keymaps/linux/jetbrains.json+11−2
@@ -3,7 +3,14 @@ · 2 unmodified lines
33 "bindings": {
44 "ctrl-alt-s": "zed::OpenSettings",
55 "ctrl-{": "pane::ActivatePreviousItem",
6− "ctrl-}": "pane::ActivateNextItem"
6+ "ctrl-}": "pane::ActivateNextItem",
7+ "ctrl-f2": "debugger::Stop",
8+ "f6": "debugger::Pause",
9+ "f7": "debugger::StepInto",
10+ "f8": "debugger::StepOver",
11+ "shift-f8": "debugger::StepOut",
12+ "f9": "debugger::Continue",
13+ "alt-shift-f9": "debugger::Start"
714 }
815 },
916 {
@@ -49,7 +56,9 @@ · 39 unmodified lines
4956 "ctrl-home": "editor::MoveToBeginning",
5057 "ctrl-end": "editor::MoveToEnd",
5158 "ctrl-shift-home": "editor::SelectToBeginning",
52− "ctrl-shift-end": "editor::SelectToEnd"
59+ "ctrl-shift-end": "editor::SelectToEnd",
60+ "ctrl-f8": "editor::ToggleBreakpoint",
61+ "ctrl-shift-f8": "editor::EditLogBreakpoint"
5362 }
5463 },
5564 {
assets/keymaps/macos/jetbrains.json+11−2
@@ -2,7 +2,14 @@ · 1 unmodified line
22 {
33 "bindings": {
44 "cmd-{": "pane::ActivatePreviousItem",
5− "cmd-}": "pane::ActivateNextItem"
5+ "cmd-}": "pane::ActivateNextItem",
6+ "ctrl-f2": "debugger::Stop",
7+ "f6": "debugger::Pause",
8+ "f7": "debugger::StepInto",
9+ "f8": "debugger::StepOver",
10+ "shift-f8": "debugger::StepOut",
11+ "f9": "debugger::Continue",
12+ "alt-shift-f9": "debugger::Start"
613 }
714 },
815 {
@@ -46,7 +53,9 @@ · 37 unmodified lines
4653 "cmd-home": "editor::MoveToBeginning",
4754 "cmd-end": "editor::MoveToEnd",
4855 "cmd-shift-home": "editor::SelectToBeginning",
49− "cmd-shift-end": "editor::SelectToEnd"
56+ "cmd-shift-end": "editor::SelectToEnd",
57+ "ctrl-f8": "editor::ToggleBreakpoint",
58+ "ctrl-shift-f8": "editor::EditLogBreakpoint"
5059 }
5160 },
5261 {
assets/settings/default.json+9−1
@@ -324,6 +324,8 @@ · 323 unmodified lines
324324 "code_actions": true,
325325 // Whether to show runnables buttons in the gutter.
326326 "runnables": true,
327+ // Whether to show breakpoints in the gutter.
328+ "breakpoints": true,
327329 // Whether to show fold buttons in the gutter.
328330 "folds": true
329331 },
@@ -1453,6 +1455,12 @@ · 1123 unmodified lines
14531455 // }
14541456 // ]
14551457 "ssh_connections": [],
1458+
14561459 // Configures context servers for use in the Assistant.
1457− "context_servers": {}
1460+ "context_servers": {},
1461+ "debugger": {
1462+ "stepping_granularity": "line",
1463+ "save_breakpoints": true,
1464+ "button": true
1465+ }
14581466 }
assets/settings/initial_debug_tasks.json+32−0
@@ -0,0 +1,32 @@
1+[
2+ {
3+ "label": "Debug active PHP file",
4+ "adapter": "php",
5+ "program": "$ZED_FILE",
6+ "request": "launch",
7+ "cwd": "$ZED_WORKTREE_ROOT"
8+ },
9+ {
10+ "label": "Debug active Python file",
11+ "adapter": "python",
12+ "program": "$ZED_FILE",
13+ "request": "launch",
14+ "cwd": "$ZED_WORKTREE_ROOT"
15+ },
16+ {
17+ "label": "Debug active JavaScript file",
18+ "adapter": "javascript",
19+ "program": "$ZED_FILE",
20+ "request": "launch",
21+ "cwd": "$ZED_WORKTREE_ROOT"
22+ },
23+ {
24+ "label": "JavaScript debug terminal",
25+ "adapter": "javascript",
26+ "request": "launch",
27+ "cwd": "$ZED_WORKTREE_ROOT",
28+ "initialize_args": {
29+ "console": "integratedTerminal"
30+ }
31+ }
32+]
crates/activity_indicator/Cargo.toml+0−1
@@ -20,7 +20,6 @@ extension_host.workspace = true · 19 unmodified lines
2020 futures.workspace = true
2121 gpui.workspace = true
2222 language.workspace = true
23−lsp.workspace = true
2423 project.workspace = true
2524 smallvec.workspace = true
2625 ui.workspace = true
crates/activity_indicator/src/activity_indicator.rs+32−22
@@ -7,8 +7,7 @@ use gpui::{ · 6 unmodified lines
77 EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
88 StatefulInteractiveElement, Styled, Transformation, Window,
99 };
10−use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
11−use lsp::LanguageServerName;
10+use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
1211 use project::{
1312 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
1413 ProjectEnvironmentEvent, WorktreeId,
@@ -23,21 +22,21 @@ actions!(activity_indicator, [ShowErrorMessage]); · 8 unmodified lines
2322
2423 pub enum Event {
2524 ShowError {
26− lsp_name: LanguageServerName,
25+ server_name: SharedString,
2726 error: String,
2827 },
2928 }
3029
3130 pub struct ActivityIndicator {
32− statuses: Vec<LspStatus>,
31+ statuses: Vec<ServerStatus>,
3332 project: Entity<Project>,
3433 auto_updater: Option<Entity<AutoUpdater>>,
3534 context_menu_handle: PopoverMenuHandle<ContextMenu>,
3635 }
3736
38−struct LspStatus {
39− name: LanguageServerName,
40− status: LanguageServerBinaryStatus,
37+struct ServerStatus {
38+ name: SharedString,
39+ status: BinaryStatus,
4140 }
4241
4342 struct PendingWork<'a> {
@@ -68,7 +67,20 @@ impl ActivityIndicator { · 24 unmodified lines
6867 while let Some((name, status)) = status_events.next().await {
6968 this.update(&mut cx, |this: &mut ActivityIndicator, cx| {
7069 this.statuses.retain(|s| s.name != name);
71− this.statuses.push(LspStatus { name, status });
70+ this.statuses.push(ServerStatus { name, status });
71+ cx.notify();
72+ })?;
73+ }
74+ anyhow::Ok(())
75+ })
76+ .detach();
77+
78+ let mut status_events = languages.dap_server_binary_statuses();
79+ cx.spawn(|this, mut cx| async move {
80+ while let Some((name, status)) = status_events.next().await {
81+ this.update(&mut cx, |this, cx| {
82+ this.statuses.retain(|s| s.name != name);
83+ this.statuses.push(ServerStatus { name, status });
7284 cx.notify();
7385 })?;
7486 }
@@ -106,18 +118,18 @@ impl ActivityIndicator { · 31 unmodified lines
106118 });
107119
108120 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
109− Event::ShowError { lsp_name, error } => {
121+ Event::ShowError { server_name, error } => {
110122 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
111123 let project = project.clone();
112124 let error = error.clone();
113− let lsp_name = lsp_name.clone();
125+ let server_name = server_name.clone();
114126 cx.spawn_in(window, |workspace, mut cx| async move {
115127 let buffer = create_buffer.await?;
116128 buffer.update(&mut cx, |buffer, cx| {
117129 buffer.edit(
118130 [(
119131 0..0,
120− format!("Language server error: {}\n\n{}", lsp_name, error),
132+ format!("Language server error: {}\n\n{}", server_name, error),
121133 )],
122134 None,
123135 cx,
@@ -147,9 +159,9 @@ impl ActivityIndicator { · 23 unmodified lines
147159
148160 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
149161 self.statuses.retain(|status| {
150− if let LanguageServerBinaryStatus::Failed { error } = &status.status {
162+ if let BinaryStatus::Failed { error } = &status.status {
151163 cx.emit(Event::ShowError {
152− lsp_name: status.name.clone(),
164+ server_name: status.name.clone(),
153165 error: error.clone(),
154166 });
155167 false
@@ -278,12 +290,10 @@ impl ActivityIndicator { · 122 unmodified lines
278290 let mut failed = SmallVec::<[_; 3]>::new();
279291 for status in &self.statuses {
280292 match status.status {
281− LanguageServerBinaryStatus::CheckingForUpdate => {
282− checking_for_update.push(status.name.clone())
283− }
284− LanguageServerBinaryStatus::Downloading => downloading.push(status.name.clone()),
285− LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.clone()),
286− LanguageServerBinaryStatus::None => {}
293+ BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
294+ BinaryStatus::Downloading => downloading.push(status.name.clone()),
295+ BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
296+ BinaryStatus::None => {}
287297 }
288298 }
289299
@@ -296,7 +306,7 @@ impl ActivityIndicator { · 6 unmodified lines
296306 ),
297307 message: format!(
298308 "Downloading {}...",
299− downloading.iter().map(|name| name.0.as_ref()).fold(
309+ downloading.iter().map(|name| name.as_ref()).fold(
300310 String::new(),
301311 |mut acc, s| {
302312 if !acc.is_empty() {
@@ -324,7 +334,7 @@ impl ActivityIndicator { · 21 unmodified lines
324334 ),
325335 message: format!(
326336 "Checking for updates to {}...",
327− checking_for_update.iter().map(|name| name.0.as_ref()).fold(
337+ checking_for_update.iter().map(|name| name.as_ref()).fold(
328338 String::new(),
329339 |mut acc, s| {
330340 if !acc.is_empty() {
@@ -354,7 +364,7 @@ impl ActivityIndicator { · 23 unmodified lines
354364 "Failed to run {}. Click to show error.",
355365 failed
356366 .iter()
357− .map(|name| name.0.as_ref())
367+ .map(|name| name.as_ref())
358368 .fold(String::new(), |mut acc, s| {
359369 if !acc.is_empty() {
360370 acc.push_str(", ");
crates/assistant_context_editor/src/context_editor.rs+1−0
@@ -229,6 +229,7 @@ impl ContextEditor { · 228 unmodified lines
229229 editor.set_show_git_diff_gutter(false, cx);
230230 editor.set_show_code_actions(false, cx);
231231 editor.set_show_runnables(false, cx);
232+ editor.set_show_breakpoints(false, cx);
232233 editor.set_show_wrap_guides(false, cx);
233234 editor.set_show_indent_guides(false, cx);
234235 editor.set_completion_provider(Some(Box::new(completion_provider)));
crates/collab/Cargo.toml+3−0
@@ -89,8 +89,11 @@ channel.workspace = true · 88 unmodified lines
8989 client = { workspace = true, features = ["test-support"] }
9090 collab_ui = { workspace = true, features = ["test-support"] }
9191 collections = { workspace = true, features = ["test-support"] }
92+command_palette_hooks.workspace = true
9293 context_server.workspace = true
9394 ctor.workspace = true
95+dap = { workspace = true, features = ["test-support"] }
96+debugger_ui = { workspace = true, features = ["test-support"] }
9497 editor = { workspace = true, features = ["test-support"] }
9598 env_logger.workspace = true
9699 extension.workspace = true
crates/collab/migrations.sqlite/20221109000000_test_schema.sql+11−0
@@ -469,3 +469,14 @@ CREATE TABLE IF NOT EXISTS processed_stripe_events ( · 468 unmodified lines
469469 );
470470
471471 CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
472+
473+CREATE TABLE IF NOT EXISTS "breakpoints" (
474+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
475+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
476+ "position" INTEGER NOT NULL,
477+ "log_message" TEXT NULL,
478+ "worktree_id" BIGINT NOT NULL,
479+ "path" TEXT NOT NULL,
480+ "kind" VARCHAR NOT NULL
481+);
482+CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
crates/collab/migrations/20241121185750_add_breakpoints.sql+11−0
@@ -0,0 +1,11 @@
1+CREATE TABLE IF NOT EXISTS "breakpoints" (
2+ "id" SERIAL PRIMARY KEY,
3+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
4+ "position" INTEGER NOT NULL,
5+ "log_message" TEXT NULL,
6+ "worktree_id" BIGINT NOT NULL,
7+ "path" TEXT NOT NULL,
8+ "kind" VARCHAR NOT NULL
9+);
10+
11+CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
crates/collab/src/db/queries/projects.rs+40−29
@@ -1,5 +1,5 @@
11 use anyhow::Context as _;
2−
2+use collections::HashSet;
33 use util::ResultExt;
44
55 use super::*;
@@ -1106,39 +1106,50 @@ impl Database { · 1100 unmodified lines
11061106 exclude_dev_server: bool,
11071107 ) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
11081108 self.project_transaction(project_id, |tx| async move {
1109− let project = project::Entity::find_by_id(project_id)
1110− .one(&*tx)
1111− .await?
1112− .ok_or_else(|| anyhow!("no such project"))?;
1109+ self.internal_project_connection_ids(project_id, connection_id, exclude_dev_server, &tx)
1110+ .await
1111+ })
1112+ .await
1113+ }
11131114
1114− let mut collaborators = project_collaborator::Entity::find()
1115− .filter(project_collaborator::Column::ProjectId.eq(project_id))
1116− .stream(&*tx)
1117− .await?;
1115+ async fn internal_project_connection_ids(
1116+ &self,
1117+ project_id: ProjectId,
1118+ connection_id: ConnectionId,
1119+ exclude_dev_server: bool,
1120+ tx: &DatabaseTransaction,
1121+ ) -> Result<HashSet<ConnectionId>> {
1122+ let project = project::Entity::find_by_id(project_id)
1123+ .one(tx)
1124+ .await?
1125+ .ok_or_else(|| anyhow!("no such project"))?;
11181126
1119− let mut connection_ids = HashSet::default();
1120− if let Some(host_connection) = project.host_connection().log_err() {
1121− if !exclude_dev_server {
1122− connection_ids.insert(host_connection);
1123− }
1124− }
1127+ let mut collaborators = project_collaborator::Entity::find()
1128+ .filter(project_collaborator::Column::ProjectId.eq(project_id))
1129+ .stream(tx)
1130+ .await?;
11251131
1126− while let Some(collaborator) = collaborators.next().await {
1127− let collaborator = collaborator?;
1128− connection_ids.insert(collaborator.connection());
1132+ let mut connection_ids = HashSet::default();
1133+ if let Some(host_connection) = project.host_connection().log_err() {
1134+ if !exclude_dev_server {
1135+ connection_ids.insert(host_connection);
11291136 }
1137+ }
11301138
1131− if connection_ids.contains(&connection_id)
1132− || Some(connection_id) == project.host_connection().ok()
1133− {
1134− Ok(connection_ids)
1135− } else {
1136− Err(anyhow!(
1137− "can only send project updates to a project you're in"
1138− ))?
1139− }
1140− })
1141− .await
1139+ while let Some(collaborator) = collaborators.next().await {
1140+ let collaborator = collaborator?;
1141+ connection_ids.insert(collaborator.connection());
1142+ }
1143+
1144+ if connection_ids.contains(&connection_id)
1145+ || Some(connection_id) == project.host_connection().ok()
1146+ {
1147+ Ok(connection_ids)
1148+ } else {
1149+ Err(anyhow!(
1150+ "can only send project updates to a project you're in"
1151+ ))?
1152+ }
11421153 }
11431154
11441155 async fn project_guest_connection_ids(
crates/collab/src/rpc.rs+3−1
@@ -404,6 +404,8 @@ impl Server { · 403 unmodified lines
404404 .add_request_handler(forward_read_only_project_request::<proto::GitReset>)
405405 .add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
406406 .add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
407+ .add_request_handler(forward_mutating_project_request::<proto::ToggleBreakpoint>)
408+ .add_message_handler(broadcast_project_message_from_host::<proto::BreakpointsForFile>)
407409 .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
408410 .add_request_handler(forward_mutating_project_request::<proto::GitDiff>)
409411 .add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
@@ -2064,7 +2066,7 @@ async fn update_worktree_settings( · 1654 unmodified lines
20642066 Ok(())
20652067 }
20662068
2067−/// Notify other participants that a language server has started.
2069+/// Notify other participants that a language server has started.
20682070 async fn start_language_server(
20692071 request: proto::StartLanguageServer,
20702072 session: Session,
crates/collab/src/tests.rs+1−0
@@ -11,6 +11,7 @@ mod channel_buffer_tests; · 10 unmodified lines
1111 mod channel_guest_tests;
1212 mod channel_message_tests;
1313 mod channel_tests;
14+// mod debug_panel_tests;
1415 mod editor_tests;
1516 mod following_tests;
1617 mod git_tests;
crates/collab/src/tests/debug_panel_tests.rs+2454−0
@@ -0,0 +1,2454 @@
1+use call::ActiveCall;
2+use dap::requests::{Initialize, Launch, StackTrace};
3+use dap::DebugRequestType;
4+use dap::{requests::SetBreakpoints, SourceBreakpoint};
5+use debugger_ui::debugger_panel::DebugPanel;
6+use debugger_ui::session::DebugSession;
7+use editor::Editor;
8+use gpui::{Entity, TestAppContext, VisualTestContext};
9+use project::{Project, ProjectPath, WorktreeId};
10+use serde_json::json;
11+use std::sync::Arc;
12+use std::{
13+ path::Path,
14+ sync::atomic::{AtomicBool, Ordering},
15+};
16+use workspace::{dock::Panel, Workspace};
17+
18+use super::{TestClient, TestServer};
19+
20+pub fn init_test(cx: &mut gpui::TestAppContext) {
21+ if std::env::var("RUST_LOG").is_ok() {
22+ env_logger::try_init().ok();
23+ }
24+
25+ cx.update(|cx| {
26+ theme::init(theme::LoadThemes::JustBase, cx);
27+ command_palette_hooks::init(cx);
28+ language::init(cx);
29+ workspace::init_settings(cx);
30+ project::Project::init_settings(cx);
31+ debugger_ui::init(cx);
32+ editor::init(cx);
33+ });
34+}
35+
36+async fn add_debugger_panel(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) {
37+ let debugger_panel = workspace
38+ .update_in(cx, |_workspace, window, cx| {
39+ cx.spawn_in(window, DebugPanel::load)
40+ })
41+ .await
42+ .unwrap();
43+
44+ workspace.update_in(cx, |workspace, window, cx| {
45+ workspace.add_panel(debugger_panel, window, cx);
46+ });
47+}
48+
49+pub fn _active_session(
50+ workspace: Entity<Workspace>,
51+ cx: &mut VisualTestContext,
52+) -> Entity<DebugSession> {
53+ workspace.update_in(cx, |workspace, _window, cx| {
54+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
55+ debug_panel
56+ .update(cx, |this, cx| this.active_session(cx))
57+ .unwrap()
58+ })
59+}
60+
61+struct ZedInstance<'a> {
62+ client: TestClient,
63+ project: Option<Entity<Project>>,
64+ active_call: Entity<ActiveCall>,
65+ cx: &'a mut TestAppContext,
66+}
67+
68+impl<'a> ZedInstance<'a> {
69+ fn new(client: TestClient, cx: &'a mut TestAppContext) -> Self {
70+ ZedInstance {
71+ project: None,
72+ client,
73+ active_call: cx.read(ActiveCall::global),
74+ cx,
75+ }
76+ }
77+
78+ async fn host_project(
79+ &mut self,
80+ project_files: Option<serde_json::Value>,
81+ ) -> (u64, WorktreeId) {
82+ let (project, worktree_id) = self.client.build_local_project("/project", self.cx).await;
83+ self.active_call
84+ .update(self.cx, |call, cx| call.set_location(Some(&project), cx))
85+ .await
86+ .unwrap();
87+
88+ if let Some(tree) = project_files {
89+ self.client.fs().insert_tree("/project", tree).await;
90+ }
91+
92+ self.project = Some(project.clone());
93+
94+ let project_id = self
95+ .active_call
96+ .update(self.cx, |call, cx| call.share_project(project, cx))
97+ .await
98+ .unwrap();
99+
100+ (project_id, worktree_id)
101+ }
102+
103+ async fn join_project(&mut self, project_id: u64) {
104+ let remote_project = self.client.join_remote_project(project_id, self.cx).await;
105+ self.project = Some(remote_project);
106+
107+ self.active_call
108+ .update(self.cx, |call, cx| {
109+ call.set_location(self.project.as_ref(), cx)
110+ })
111+ .await
112+ .unwrap();
113+ }
114+
115+ async fn expand(
116+ &'a mut self,
117+ ) -> (
118+ &'a TestClient,
119+ Entity<Workspace>,
120+ Entity<Project>,
121+ &'a mut VisualTestContext,
122+ ) {
123+ let (workspace, cx) = self.client.build_workspace(
124+ self.project
125+ .as_ref()
126+ .expect("Project should be hosted or built before expanding"),
127+ self.cx,
128+ );
129+ add_debugger_panel(&workspace, cx).await;
130+ (&self.client, workspace, self.project.clone().unwrap(), cx)
131+ }
132+}
133+
134+async fn _setup_three_member_test<'a, 'b, 'c>(
135+ server: &mut TestServer,
136+ host_cx: &'a mut TestAppContext,
137+ first_remote_cx: &'b mut TestAppContext,
138+ second_remote_cx: &'c mut TestAppContext,
139+) -> (ZedInstance<'a>, ZedInstance<'b>, ZedInstance<'c>) {
140+ let host_client = server.create_client(host_cx, "user_host").await;
141+ let first_remote_client = server.create_client(first_remote_cx, "user_remote_1").await;
142+ let second_remote_client = server
143+ .create_client(second_remote_cx, "user_remote_2")
144+ .await;
145+
146+ init_test(host_cx);
147+ init_test(first_remote_cx);
148+ init_test(second_remote_cx);
149+
150+ server
151+ .create_room(&mut [
152+ (&host_client, host_cx),
153+ (&first_remote_client, first_remote_cx),
154+ (&second_remote_client, second_remote_cx),
155+ ])
156+ .await;
157+
158+ let host_zed = ZedInstance::new(host_client, host_cx);
159+ let first_remote_zed = ZedInstance::new(first_remote_client, first_remote_cx);
160+ let second_remote_zed = ZedInstance::new(second_remote_client, second_remote_cx);
161+
162+ (host_zed, first_remote_zed, second_remote_zed)
163+}
164+
165+async fn setup_two_member_test<'a, 'b>(
166+ server: &mut TestServer,
167+ host_cx: &'a mut TestAppContext,
168+ remote_cx: &'b mut TestAppContext,
169+) -> (ZedInstance<'a>, ZedInstance<'b>) {
170+ let host_client = server.create_client(host_cx, "user_host").await;
171+ let remote_client = server.create_client(remote_cx, "user_remote").await;
172+
173+ init_test(host_cx);
174+ init_test(remote_cx);
175+
176+ server
177+ .create_room(&mut [(&host_client, host_cx), (&remote_client, remote_cx)])
178+ .await;
179+
180+ let host_zed = ZedInstance::new(host_client, host_cx);
181+ let remote_zed = ZedInstance::new(remote_client, remote_cx);
182+
183+ (host_zed, remote_zed)
184+}
185+
186+#[gpui::test]
187+async fn test_debug_panel_item_opens_on_remote(
188+ host_cx: &mut TestAppContext,
189+ remote_cx: &mut TestAppContext,
190+) {
191+ let executor = host_cx.executor();
192+ let mut server = TestServer::start(executor).await;
193+
194+ let (mut host_zed, mut remote_zed) =
195+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
196+
197+ let (host_project_id, _) = host_zed.host_project(None).await;
198+ remote_zed.join_project(host_project_id).await;
199+
200+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
201+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
202+
203+ remote_cx.run_until_parked();
204+
205+ let task = host_project.update(host_cx, |project, cx| {
206+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
207+ });
208+
209+ let session = task.await.unwrap();
210+ let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
211+
212+ client
213+ .on_request::<Initialize, _>(move |_, _| {
214+ Ok(dap::Capabilities {
215+ supports_step_back: Some(false),
216+ ..Default::default()
217+ })
218+ })
219+ .await;
220+
221+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
222+
223+ client
224+ .on_request::<StackTrace, _>(move |_, _| {
225+ Ok(dap::StackTraceResponse {
226+ stack_frames: Vec::default(),
227+ total_frames: None,
228+ })
229+ })
230+ .await;
231+
232+ client
233+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
234+ reason: dap::StoppedEventReason::Pause,
235+ description: None,
236+ thread_id: Some(1),
237+ preserve_focus_hint: None,
238+ text: None,
239+ all_threads_stopped: None,
240+ hit_breakpoint_ids: None,
241+ }))
242+ .await;
243+
244+ host_cx.run_until_parked();
245+ remote_cx.run_until_parked();
246+
247+ remote_workspace.update(remote_cx, |workspace, cx| {
248+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
249+ let _active_session = debug_panel
250+ .update(cx, |this, cx| this.active_session(cx))
251+ .unwrap();
252+
253+ assert_eq!(
254+ 1,
255+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
256+ );
257+ // assert_eq!(client.id(), active_session.read(cx).());
258+ // assert_eq!(1, active_session.read(cx).thread_id().0);
259+ // todo(debugger) check selected thread id
260+ });
261+
262+ let shutdown_client = host_project.update(host_cx, |project, cx| {
263+ project.dap_store().update(cx, |dap_store, cx| {
264+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
265+ })
266+ });
267+
268+ shutdown_client.await.unwrap();
269+}
270+
271+#[gpui::test]
272+async fn test_active_debug_panel_item_set_on_join_project(
273+ host_cx: &mut TestAppContext,
274+ remote_cx: &mut TestAppContext,
275+) {
276+ let executor = host_cx.executor();
277+ let mut server = TestServer::start(executor).await;
278+
279+ let (mut host_zed, mut remote_zed) =
280+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
281+
282+ let (host_project_id, _) = host_zed.host_project(None).await;
283+
284+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
285+
286+ host_cx.run_until_parked();
287+
288+ let task = host_project.update(host_cx, |project, cx| {
289+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
290+ });
291+
292+ let session = task.await.unwrap();
293+ let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
294+
295+ client
296+ .on_request::<Initialize, _>(move |_, _| {
297+ Ok(dap::Capabilities {
298+ supports_step_back: Some(false),
299+ ..Default::default()
300+ })
301+ })
302+ .await;
303+
304+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
305+
306+ client
307+ .on_request::<StackTrace, _>(move |_, _| {
308+ Ok(dap::StackTraceResponse {
309+ stack_frames: Vec::default(),
310+ total_frames: None,
311+ })
312+ })
313+ .await;
314+
315+ client
316+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
317+ reason: dap::StoppedEventReason::Pause,
318+ description: None,
319+ thread_id: Some(1),
320+ preserve_focus_hint: None,
321+ text: None,
322+ all_threads_stopped: None,
323+ hit_breakpoint_ids: None,
324+ }))
325+ .await;
326+
327+ // Give host_client time to send a debug panel item to collab server
328+ host_cx.run_until_parked();
329+
330+ remote_zed.join_project(host_project_id).await;
331+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
332+
333+ host_cx.run_until_parked();
334+ remote_cx.run_until_parked();
335+
336+ remote_workspace.update(remote_cx, |workspace, cx| {
337+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
338+ let _active_session = debug_panel
339+ .update(cx, |this, cx| this.active_session(cx))
340+ .unwrap();
341+
342+ assert_eq!(
343+ 1,
344+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
345+ );
346+ // assert_eq!(cl, active_session.read(cx).client_id());
347+ // assert_eq!(1, active_session.read(cx).thread_id().0);
348+ // todo(debugger)
349+ });
350+
351+ let shutdown_client = host_project.update(host_cx, |project, cx| {
352+ project.dap_store().update(cx, |dap_store, cx| {
353+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
354+ })
355+ });
356+
357+ shutdown_client.await.unwrap();
358+
359+ remote_cx.run_until_parked();
360+
361+ // assert we don't have a debug panel item anymore because the client shutdown
362+ remote_workspace.update(remote_cx, |workspace, cx| {
363+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
364+
365+ debug_panel.update(cx, |this, cx| {
366+ assert!(this.active_session(cx).is_none());
367+ assert_eq!(0, this.pane().unwrap().read(cx).items_len());
368+ });
369+ });
370+}
371+
372+#[gpui::test]
373+async fn test_debug_panel_remote_button_presses(
374+ _host_cx: &mut TestAppContext,
375+ _remote_cx: &mut TestAppContext,
376+) {
377+ unimplemented!("Collab is still being refactored");
378+ // let executor = host_cx.executor();
379+ // let mut server = TestServer::start(executor).await;
380+
381+ // let (mut host_zed, mut remote_zed) =
382+ // setup_two_member_test(&mut server, host_cx, remote_cx).await;
383+
384+ // let (host_project_id, _) = host_zed.host_project(None).await;
385+ // remote_zed.join_project(host_project_id).await;
386+
387+ // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
388+ // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
389+
390+ // let task = host_project.update(host_cx, |project, cx| {
391+ // project.start_debug_session(dap::test_config(None), cx)
392+ // });
393+
394+ // let session = task.await.unwrap();
395+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
396+
397+ // client
398+ // .on_request::<Initialize, _>(move |_, _| {
399+ // Ok(dap::Capabilities {
400+ // supports_step_back: Some(true),
401+ // ..Default::default()
402+ // })
403+ // })
404+ // .await;
405+
406+ // client.on_request::<Launch, _>(move |_, _| Ok(())).await;
407+
408+ // client
409+ // .on_request::<StackTrace, _>(move |_, _| {
410+ // Ok(dap::StackTraceResponse {
411+ // stack_frames: Vec::default(),
412+ // total_frames: None,
413+ // })
414+ // })
415+ // .await;
416+
417+ // client
418+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
419+ // reason: dap::StoppedEventReason::Pause,
420+ // description: None,
421+ // thread_id: Some(1),
422+ // preserve_focus_hint: None,
423+ // text: None,
424+ // all_threads_stopped: None,
425+ // hit_breakpoint_ids: None,
426+ // }))
427+ // .await;
428+
429+ // client
430+ // .on_request::<dap::requests::Continue, _>(move |_, _| {
431+ // Ok(dap::ContinueResponse {
432+ // all_threads_continued: Some(true),
433+ // })
434+ // })
435+ // .await;
436+
437+ // host_cx.run_until_parked();
438+ // remote_cx.run_until_parked();
439+
440+ // let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
441+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
442+ // let active_session = debug_panel
443+ // .update(cx, |this, cx| this.active_session(cx))
444+ // .unwrap();
445+
446+ // assert_eq!(
447+ // 1,
448+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
449+ // );
450+ // // assert_eq!(client.id(), active_session.read(cx).client_id());
451+ // // assert_eq!(1, active_session.read(cx).thread_id().0);
452+ // // todo(debugger)
453+ // active_session
454+ // });
455+
456+ // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
457+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
458+ // let active_session = debug_panel
459+ // .update(cx, |this, cx| this.active_session(cx))
460+ // .unwrap();
461+
462+ // assert_eq!(
463+ // 1,
464+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
465+ // );
466+ // // assert_eq!(client.id(), active_session.read(cx).client_id());
467+ // // assert_eq!(1, active_session.read(cx).thread_id().0);
468+ // // todo(debugger)
469+ // active_session
470+ // });
471+
472+ // remote_debug_item.update(remote_cx, |this, cx| {
473+ // this.continue_thread(cx);
474+ // });
475+
476+ // host_cx.run_until_parked();
477+ // remote_cx.run_until_parked();
478+
479+ // local_debug_item.update(host_cx, |debug_panel_item, cx| {
480+ // assert_eq!(
481+ // debugger_ui::debugger_panel::ThreadStatus::Running,
482+ // debug_panel_item.thread_state().read(cx).status,
483+ // );
484+ // });
485+
486+ // remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
487+ // assert_eq!(
488+ // debugger_ui::debugger_panel::ThreadStatus::Running,
489+ // debug_panel_item.thread_state().read(cx).status,
490+ // );
491+ // });
492+
493+ // client
494+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
495+ // reason: dap::StoppedEventReason::Pause,
496+ // description: None,
497+ // thread_id: Some(1),
498+ // preserve_focus_hint: None,
499+ // text: None,
500+ // all_threads_stopped: None,
501+ // hit_breakpoint_ids: None,
502+ // }))
503+ // .await;
504+
505+ // client
506+ // .on_request::<StackTrace, _>(move |_, _| {
507+ // Ok(dap::StackTraceResponse {
508+ // stack_frames: Vec::default(),
509+ // total_frames: None,
510+ // })
511+ // })
512+ // .await;
513+
514+ // host_cx.run_until_parked();
515+ // remote_cx.run_until_parked();
516+
517+ // local_debug_item.update(host_cx, |debug_panel_item, cx| {
518+ // assert_eq!(
519+ // debugger_ui::debugger_panel::ThreadStatus::Stopped,
520+ // debug_panel_item.thread_state().read(cx).status,
521+ // );
522+ // });
523+
524+ // remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
525+ // assert_eq!(
526+ // debugger_ui::debugger_panel::ThreadStatus::Stopped,
527+ // debug_panel_item.thread_state().read(cx).status,
528+ // );
529+ // });
530+
531+ // client
532+ // .on_request::<dap::requests::Continue, _>(move |_, _| {
533+ // Ok(dap::ContinueResponse {
534+ // all_threads_continued: Some(true),
535+ // })
536+ // })
537+ // .await;
538+
539+ // local_debug_item.update(host_cx, |this, cx| {
540+ // this.continue_thread(cx);
541+ // });
542+
543+ // host_cx.run_until_parked();
544+ // remote_cx.run_until_parked();
545+
546+ // local_debug_item.update(host_cx, |debug_panel_item, cx| {
547+ // assert_eq!(
548+ // debugger_ui::debugger_panel::ThreadStatus::Running,
549+ // debug_panel_item.thread_state().read(cx).status,
550+ // );
551+ // });
552+
553+ // remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
554+ // assert_eq!(
555+ // debugger_ui::debugger_panel::ThreadStatus::Running,
556+ // debug_panel_item.thread_state().read(cx).status,
557+ // );
558+ // });
559+
560+ // client
561+ // .on_request::<dap::requests::Pause, _>(move |_, _| Ok(()))
562+ // .await;
563+
564+ // client
565+ // .on_request::<StackTrace, _>(move |_, _| {
566+ // Ok(dap::StackTraceResponse {
567+ // stack_frames: Vec::default(),
568+ // total_frames: None,
569+ // })
570+ // })
571+ // .await;
572+
573+ // client
574+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
575+ // reason: dap::StoppedEventReason::Pause,
576+ // description: None,
577+ // thread_id: Some(1),
578+ // preserve_focus_hint: None,
579+ // text: None,
580+ // all_threads_stopped: None,
581+ // hit_breakpoint_ids: None,
582+ // }))
583+ // .await;
584+
585+ // remote_debug_item.update(remote_cx, |this, cx| {
586+ // this.pause_thread(cx);
587+ // });
588+
589+ // remote_cx.run_until_parked();
590+ // host_cx.run_until_parked();
591+
592+ // client
593+ // .on_request::<dap::requests::StepOut, _>(move |_, _| Ok(()))
594+ // .await;
595+
596+ // remote_debug_item.update(remote_cx, |this, cx| {
597+ // this.step_out(cx);
598+ // });
599+
600+ // client
601+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
602+ // reason: dap::StoppedEventReason::Pause,
603+ // description: None,
604+ // thread_id: Some(1),
605+ // preserve_focus_hint: None,
606+ // text: None,
607+ // all_threads_stopped: None,
608+ // hit_breakpoint_ids: None,
609+ // }))
610+ // .await;
611+
612+ // remote_cx.run_until_parked();
613+ // host_cx.run_until_parked();
614+
615+ // client
616+ // .on_request::<dap::requests::Next, _>(move |_, _| Ok(()))
617+ // .await;
618+
619+ // remote_debug_item.update(remote_cx, |this, cx| {
620+ // this.step_over(cx);
621+ // });
622+
623+ // client
624+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
625+ // reason: dap::StoppedEventReason::Pause,
626+ // description: None,
627+ // thread_id: Some(1),
628+ // preserve_focus_hint: None,
629+ // text: None,
630+ // all_threads_stopped: None,
631+ // hit_breakpoint_ids: None,
632+ // }))
633+ // .await;
634+
635+ // remote_cx.run_until_parked();
636+ // host_cx.run_until_parked();
637+
638+ // client
639+ // .on_request::<dap::requests::StepIn, _>(move |_, _| Ok(()))
640+ // .await;
641+
642+ // remote_debug_item.update(remote_cx, |this, cx| {
643+ // this.step_in(cx);
644+ // });
645+
646+ // client
647+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
648+ // reason: dap::StoppedEventReason::Pause,
649+ // description: None,
650+ // thread_id: Some(1),
651+ // preserve_focus_hint: None,
652+ // text: None,
653+ // all_threads_stopped: None,
654+ // hit_breakpoint_ids: None,
655+ // }))
656+ // .await;
657+
658+ // remote_cx.run_until_parked();
659+ // host_cx.run_until_parked();
660+
661+ // client
662+ // .on_request::<dap::requests::StepBack, _>(move |_, _| Ok(()))
663+ // .await;
664+
665+ // remote_debug_item.update(remote_cx, |this, cx| {
666+ // this.step_back(cx);
667+ // });
668+
669+ // client
670+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
671+ // reason: dap::StoppedEventReason::Pause,
672+ // description: None,
673+ // thread_id: Some(1),
674+ // preserve_focus_hint: None,
675+ // text: None,
676+ // all_threads_stopped: None,
677+ // hit_breakpoint_ids: None,
678+ // }))
679+ // .await;
680+
681+ // remote_cx.run_until_parked();
682+ // host_cx.run_until_parked();
683+
684+ // remote_debug_item.update(remote_cx, |this, cx| {
685+ // this.stop_thread(cx);
686+ // });
687+
688+ // host_cx.run_until_parked();
689+ // remote_cx.run_until_parked();
690+
691+ // // assert we don't have a debug panel item anymore because the client shutdown
692+ // remote_workspace.update(remote_cx, |workspace, cx| {
693+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
694+
695+ // debug_panel.update(cx, |this, cx| {
696+ // assert!(this.active_session(cx).is_none());
697+ // assert_eq!(0, this.pane().unwrap().read(cx).items_len());
698+ // });
699+ // });
700+}
701+
702+#[gpui::test]
703+async fn test_restart_stack_frame(_host_cx: &mut TestAppContext, _remote_cx: &mut TestAppContext) {
704+ unimplemented!("Collab is still being refactored");
705+ // let executor = host_cx.executor();
706+ // let mut server = TestServer::start(executor).await;
707+
708+ // let (mut host_zed, mut remote_zed) =
709+ // setup_two_member_test(&mut server, host_cx, remote_cx).await;
710+
711+ // let (host_project_id, _) = host_zed.host_project(None).await;
712+ // remote_zed.join_project(host_project_id).await;
713+
714+ // let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
715+ // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
716+
717+ // let called_restart_frame = Arc::new(AtomicBool::new(false));
718+
719+ // let task = host_project.update(host_cx, |project, cx| {
720+ // project.start_debug_session(dap::test_config(None), cx)
721+ // });
722+
723+ // let session = task.await.unwrap();
724+ // let client = session.read(cx).adapter_client().unwrap();
725+
726+ // client
727+ // .on_request::<Initialize, _>(move |_, _| {
728+ // Ok(dap::Capabilities {
729+ // supports_restart_frame: Some(true),
730+ // ..Default::default()
731+ // })
732+ // })
733+ // .await;
734+
735+ // client.on_request::<Launch, _>(move |_, _| Ok(())).await;
736+
737+ // let stack_frames = vec![StackFrame {
738+ // id: 1,
739+ // name: "Stack Frame 1".into(),
740+ // source: Some(dap::Source {
741+ // name: Some("test.js".into()),
742+ // path: Some("/project/src/test.js".into()),
743+ // source_reference: None,
744+ // presentation_hint: None,
745+ // origin: None,
746+ // sources: None,
747+ // adapter_data: None,
748+ // checksums: None,
749+ // }),
750+ // line: 3,
751+ // column: 1,
752+ // end_line: None,
753+ // end_column: None,
754+ // can_restart: None,
755+ // instruction_pointer_reference: None,
756+ // module_id: None,
757+ // presentation_hint: None,
758+ // }];
759+
760+ // client
761+ // .on_request::<StackTrace, _>({
762+ // let stack_frames = Arc::new(stack_frames.clone());
763+ // move |_, args| {
764+ // assert_eq!(1, args.thread_id);
765+
766+ // Ok(dap::StackTraceResponse {
767+ // stack_frames: (*stack_frames).clone(),
768+ // total_frames: None,
769+ // })
770+ // }
771+ // })
772+ // .await;
773+
774+ // client
775+ // .on_request::<RestartFrame, _>({
776+ // let called_restart_frame = called_restart_frame.clone();
777+ // move |_, args| {
778+ // assert_eq!(1, args.frame_id);
779+
780+ // called_restart_frame.store(true, Ordering::SeqCst);
781+
782+ // Ok(())
783+ // }
784+ // })
785+ // .await;
786+
787+ // client
788+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
789+ // reason: dap::StoppedEventReason::Pause,
790+ // description: None,
791+ // thread_id: Some(1),
792+ // preserve_focus_hint: None,
793+ // text: None,
794+ // all_threads_stopped: None,
795+ // hit_breakpoint_ids: None,
796+ // }))
797+ // .await;
798+
799+ // host_cx.run_until_parked();
800+ // remote_cx.run_until_parked();
801+
802+ // // try to restart stack frame 1 from the guest side
803+ // remote_workspace.update(remote_cx, |workspace, cx| {
804+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
805+ // let active_session = debug_panel
806+ // .update(cx, |this, cx| this.active_session(cx))
807+ // .unwrap();
808+
809+ // active_session.update(cx, |debug_panel_item, cx| {
810+ // debug_panel_item
811+ // .stack_frame_list()
812+ // .update(cx, |stack_frame_list, cx| {
813+ // stack_frame_list.restart_stack_frame(1, cx);
814+ // });
815+ // });
816+ // });
817+
818+ // host_cx.run_until_parked();
819+ // remote_cx.run_until_parked();
820+
821+ // assert!(
822+ // called_restart_frame.load(std::sync::atomic::Ordering::SeqCst),
823+ // "Restart stack frame was not called"
824+ // );
825+
826+ // let shutdown_client = host_project.update(host_cx, |project, cx| {
827+ // project.dap_store().update(cx, |dap_store, cx| {
828+ // dap_store.shutdown_session(&session.read(cx).session_id(), cx)
829+ // })
830+ // });
831+
832+ // shutdown_client.await.unwrap();
833+}
834+
835+#[gpui::test]
836+async fn test_updated_breakpoints_send_to_dap(
837+ host_cx: &mut TestAppContext,
838+ remote_cx: &mut TestAppContext,
839+) {
840+ let executor = host_cx.executor();
841+ let mut server = TestServer::start(executor).await;
842+
843+ let (mut host_zed, mut remote_zed) =
844+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
845+
846+ let (host_project_id, worktree_id) = host_zed
847+ .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
848+ .await;
849+
850+ remote_zed.join_project(host_project_id).await;
851+
852+ let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
853+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
854+
855+ let project_path = ProjectPath {
856+ worktree_id,
857+ path: Arc::from(Path::new(&"test.txt")),
858+ };
859+
860+ let task = host_project.update(host_cx, |project, cx| {
861+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
862+ });
863+
864+ let session = task.await.unwrap();
865+ let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
866+
867+ client
868+ .on_request::<Initialize, _>(move |_, _| {
869+ Ok(dap::Capabilities {
870+ supports_restart_frame: Some(true),
871+ ..Default::default()
872+ })
873+ })
874+ .await;
875+
876+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
877+ client
878+ .on_request::<StackTrace, _>(move |_, _| {
879+ Ok(dap::StackTraceResponse {
880+ stack_frames: Vec::default(),
881+ total_frames: None,
882+ })
883+ })
884+ .await;
885+
886+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
887+ client
888+ .on_request::<SetBreakpoints, _>({
889+ let called_set_breakpoints = called_set_breakpoints.clone();
890+ move |_, args| {
891+ assert_eq!("/project/test.txt", args.source.path.unwrap());
892+ assert_eq!(
893+ vec![SourceBreakpoint {
894+ line: 3,
895+ column: None,
896+ condition: None,
897+ hit_condition: None,
898+ log_message: None,
899+ mode: None
900+ }],
901+ args.breakpoints.unwrap()
902+ );
903+ // assert!(!args.source_modified.unwrap());
904+ // todo(debugger): Implement source_modified handling
905+
906+ called_set_breakpoints.store(true, Ordering::SeqCst);
907+
908+ Ok(dap::SetBreakpointsResponse {
909+ breakpoints: Vec::default(),
910+ })
911+ }
912+ })
913+ .await;
914+
915+ client
916+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
917+ reason: dap::StoppedEventReason::Pause,
918+ description: None,
919+ thread_id: Some(1),
920+ preserve_focus_hint: None,
921+ text: None,
922+ all_threads_stopped: None,
923+ hit_breakpoint_ids: None,
924+ }))
925+ .await;
926+
927+ host_cx.run_until_parked();
928+ remote_cx.run_until_parked();
929+
930+ // Client B opens an editor.
931+ let editor_b = remote_workspace
932+ .update_in(remote_cx, |workspace, window, cx| {
933+ workspace.open_path(project_path.clone(), None, true, window, cx)
934+ })
935+ .await
936+ .unwrap()
937+ .downcast::<Editor>()
938+ .unwrap();
939+
940+ editor_b.update_in(remote_cx, |editor, window, cx| {
941+ editor.move_down(&editor::actions::MoveDown, window, cx);
942+ editor.move_down(&editor::actions::MoveDown, window, cx);
943+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
944+ });
945+
946+ // Client A opens an editor.
947+ let editor_a = host_workspace
948+ .update_in(host_cx, |workspace, window, cx| {
949+ workspace.open_path(project_path.clone(), None, true, window, cx)
950+ })
951+ .await
952+ .unwrap()
953+ .downcast::<Editor>()
954+ .unwrap();
955+
956+ host_cx.run_until_parked();
957+ remote_cx.run_until_parked();
958+
959+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
960+ client
961+ .on_request::<SetBreakpoints, _>({
962+ let called_set_breakpoints = called_set_breakpoints.clone();
963+ move |_, args| {
964+ assert_eq!("/project/test.txt", args.source.path.unwrap());
965+ assert!(args.breakpoints.unwrap().is_empty());
966+ // assert!(!args.source_modified.unwrap());
967+ // todo(debugger) Implement source modified support
968+
969+ called_set_breakpoints.store(true, Ordering::SeqCst);
970+
971+ Ok(dap::SetBreakpointsResponse {
972+ breakpoints: Vec::default(),
973+ })
974+ }
975+ })
976+ .await;
977+
978+ // remove the breakpoint that client B added
979+ editor_a.update_in(host_cx, |editor, window, cx| {
980+ editor.move_down(&editor::actions::MoveDown, window, cx);
981+ editor.move_down(&editor::actions::MoveDown, window, cx);
982+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
983+ });
984+
985+ host_cx.run_until_parked();
986+ remote_cx.run_until_parked();
987+
988+ assert!(
989+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
990+ "SetBreakpoint request must be called"
991+ );
992+
993+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
994+ client
995+ .on_request::<SetBreakpoints, _>({
996+ let called_set_breakpoints = called_set_breakpoints.clone();
997+ move |_, args| {
998+ assert_eq!("/project/test.txt", args.source.path.unwrap());
999+ let mut breakpoints = args.breakpoints.unwrap();
1000+ breakpoints.sort_by_key(|b| b.line);
1001+ assert_eq!(
1002+ vec![
1003+ SourceBreakpoint {
1004+ line: 2,
1005+ column: None,
1006+ condition: None,
1007+ hit_condition: None,
1008+ log_message: None,
1009+ mode: None
1010+ },
1011+ SourceBreakpoint {
1012+ line: 3,
1013+ column: None,
1014+ condition: None,
1015+ hit_condition: None,
1016+ log_message: None,
1017+ mode: None
1018+ }
1019+ ],
1020+ breakpoints
1021+ );
1022+ // assert!(!args.source_modified.unwrap());
1023+ // todo(debugger) Implement source modified support
1024+
1025+ called_set_breakpoints.store(true, Ordering::SeqCst);
1026+
1027+ Ok(dap::SetBreakpointsResponse {
1028+ breakpoints: Vec::default(),
1029+ })
1030+ }
1031+ })
1032+ .await;
1033+
1034+ // Add our own breakpoint now
1035+ editor_a.update_in(host_cx, |editor, window, cx| {
1036+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
1037+ editor.move_up(&editor::actions::MoveUp, window, cx);
1038+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
1039+ });
1040+
1041+ host_cx.run_until_parked();
1042+ remote_cx.run_until_parked();
1043+
1044+ assert!(
1045+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1046+ "SetBreakpoint request must be called"
1047+ );
1048+
1049+ let shutdown_client = host_project.update(host_cx, |project, cx| {
1050+ project.dap_store().update(cx, |dap_store, cx| {
1051+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
1052+ })
1053+ });
1054+
1055+ shutdown_client.await.unwrap();
1056+}
1057+
1058+#[gpui::test]
1059+async fn test_module_list(
1060+ _host_cx: &mut TestAppContext,
1061+ _remote_cx: &mut TestAppContext,
1062+ _late_join_cx: &mut TestAppContext,
1063+) {
1064+ unimplemented!("Collab is still being refactored");
1065+ // let executor = host_cx.executor();
1066+ // let mut server = TestServer::start(executor).await;
1067+
1068+ // let (mut host_zed, mut remote_zed, mut late_join_zed) =
1069+ // setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await;
1070+
1071+ // let (host_project_id, _worktree_id) = host_zed.host_project(None).await;
1072+
1073+ // remote_zed.join_project(host_project_id).await;
1074+
1075+ // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
1076+ // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
1077+
1078+ // let task = host_project.update(host_cx, |project, cx| {
1079+ // project.start_debug_session(dap::test_config(None), cx)
1080+ // });
1081+
1082+ // let session = task.await.unwrap();
1083+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
1084+
1085+ // let called_initialize = Arc::new(AtomicBool::new(false));
1086+
1087+ // client
1088+ // .on_request::<Initialize, _>({
1089+ // let called_initialize = called_initialize.clone();
1090+ // move |_, _| {
1091+ // called_initialize.store(true, Ordering::SeqCst);
1092+ // Ok(dap::Capabilities {
1093+ // supports_restart_frame: Some(true),
1094+ // supports_modules_request: Some(true),
1095+ // ..Default::default()
1096+ // })
1097+ // }
1098+ // })
1099+ // .await;
1100+
1101+ // client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1102+ // client
1103+ // .on_request::<StackTrace, _>(move |_, _| {
1104+ // Ok(dap::StackTraceResponse {
1105+ // stack_frames: Vec::default(),
1106+ // total_frames: None,
1107+ // })
1108+ // })
1109+ // .await;
1110+
1111+ // let called_modules = Arc::new(AtomicBool::new(false));
1112+ // let modules = vec![
1113+ // dap::Module {
1114+ // id: dap::ModuleId::Number(1),
1115+ // name: "First Module".into(),
1116+ // address_range: None,
1117+ // date_time_stamp: None,
1118+ // path: None,
1119+ // symbol_file_path: None,
1120+ // symbol_status: None,
1121+ // version: None,
1122+ // is_optimized: None,
1123+ // is_user_code: None,
1124+ // },
1125+ // dap::Module {
1126+ // id: dap::ModuleId::Number(2),
1127+ // name: "Second Module".into(),
1128+ // address_range: None,
1129+ // date_time_stamp: None,
1130+ // path: None,
1131+ // symbol_file_path: None,
1132+ // symbol_status: None,
1133+ // version: None,
1134+ // is_optimized: None,
1135+ // is_user_code: None,
1136+ // },
1137+ // ];
1138+
1139+ // client
1140+ // .on_request::<dap::requests::Modules, _>({
1141+ // let called_modules = called_modules.clone();
1142+ // let modules = modules.clone();
1143+ // move |_, _| unsafe {
1144+ // static mut REQUEST_COUNT: i32 = 1;
1145+ // assert_eq!(
1146+ // 1, REQUEST_COUNT,
1147+ // "This request should only be called once from the host"
1148+ // );
1149+ // REQUEST_COUNT += 1;
1150+ // called_modules.store(true, Ordering::SeqCst);
1151+
1152+ // Ok(dap::ModulesResponse {
1153+ // modules: modules.clone(),
1154+ // total_modules: Some(2u64),
1155+ // })
1156+ // }
1157+ // })
1158+ // .await;
1159+
1160+ // host_cx.run_until_parked();
1161+ // remote_cx.run_until_parked();
1162+
1163+ // assert!(
1164+ // called_initialize.load(std::sync::atomic::Ordering::SeqCst),
1165+ // "Request Initialize must be called"
1166+ // );
1167+
1168+ // client
1169+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1170+ // reason: dap::StoppedEventReason::Pause,
1171+ // description: None,
1172+ // thread_id: Some(1),
1173+ // preserve_focus_hint: None,
1174+ // text: None,
1175+ // all_threads_stopped: None,
1176+ // hit_breakpoint_ids: None,
1177+ // }))
1178+ // .await;
1179+
1180+ // host_cx.run_until_parked();
1181+ // remote_cx.run_until_parked();
1182+
1183+ // assert!(
1184+ // called_modules.load(std::sync::atomic::Ordering::SeqCst),
1185+ // "Request Modules must be called"
1186+ // );
1187+
1188+ // host_workspace.update(host_cx, |workspace, cx| {
1189+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1190+ // let debug_panel_item = debug_panel
1191+ // .update(cx, |this, cx| this.active_session(cx))
1192+ // .unwrap();
1193+
1194+ // debug_panel_item.update(cx, |item, cx| {
1195+ // assert_eq!(
1196+ // true,
1197+ // item.capabilities(cx).supports_modules_request.unwrap(),
1198+ // "Local supports modules request should be true"
1199+ // );
1200+
1201+ // let local_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
1202+
1203+ // assert_eq!(
1204+ // 2usize,
1205+ // local_module_list.len(),
1206+ // "Local module list should have two items in it"
1207+ // );
1208+ // assert_eq!(
1209+ // modules.clone(),
1210+ // local_module_list,
1211+ // "Local module list should match module list from response"
1212+ // );
1213+ // })
1214+ // });
1215+
1216+ // remote_workspace.update(remote_cx, |workspace, cx| {
1217+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1218+ // let debug_panel_item = debug_panel
1219+ // .update(cx, |this, cx| this.active_session(cx))
1220+ // .unwrap();
1221+
1222+ // debug_panel_item.update(cx, |item, cx| {
1223+ // assert_eq!(
1224+ // true,
1225+ // item.capabilities(cx).supports_modules_request.unwrap(),
1226+ // "Remote capabilities supports modules request should be true"
1227+ // );
1228+ // let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
1229+
1230+ // assert_eq!(
1231+ // 2usize,
1232+ // remote_module_list.len(),
1233+ // "Remote module list should have two items in it"
1234+ // );
1235+ // assert_eq!(
1236+ // modules.clone(),
1237+ // remote_module_list,
1238+ // "Remote module list should match module list from response"
1239+ // );
1240+ // })
1241+ // });
1242+
1243+ // late_join_zed.join_project(host_project_id).await;
1244+ // let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) =
1245+ // late_join_zed.expand().await;
1246+
1247+ // late_join_workspace.update(late_join_cx, |workspace, cx| {
1248+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1249+ // let debug_panel_item = debug_panel
1250+ // .update(cx, |this, cx| this.active_session(cx))
1251+ // .unwrap();
1252+
1253+ // debug_panel_item.update(cx, |item, cx| {
1254+ // assert_eq!(
1255+ // true,
1256+ // item.capabilities(cx).supports_modules_request.unwrap(),
1257+ // "Remote (mid session join) capabilities supports modules request should be true"
1258+ // );
1259+ // let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
1260+
1261+ // assert_eq!(
1262+ // 2usize,
1263+ // remote_module_list.len(),
1264+ // "Remote (mid session join) module list should have two items in it"
1265+ // );
1266+ // assert_eq!(
1267+ // modules.clone(),
1268+ // remote_module_list,
1269+ // "Remote (mid session join) module list should match module list from response"
1270+ // );
1271+ // })
1272+ // });
1273+
1274+ // let shutdown_client = host_project.update(host_cx, |project, cx| {
1275+ // project.dap_store().update(cx, |dap_store, cx| {
1276+ // dap_store.shutdown_session(&session.read(cx).id(), cx)
1277+ // })
1278+ // });
1279+
1280+ // shutdown_client.await.unwrap();
1281+}
1282+
1283+// #[gpui::test]
1284+// async fn test_variable_list(
1285+// host_cx: &mut TestAppContext,
1286+// remote_cx: &mut TestAppContext,
1287+// late_join_cx: &mut TestAppContext,
1288+// ) {
1289+// let executor = host_cx.executor();
1290+// let mut server = TestServer::start(executor).await;
1291+
1292+// let (mut host_zed, mut remote_zed, mut late_join_zed) =
1293+// setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await;
1294+
1295+// let (host_project_id, _worktree_id) = host_zed
1296+// .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
1297+// .await;
1298+
1299+// remote_zed.join_project(host_project_id).await;
1300+
1301+// let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
1302+// let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
1303+
1304+// let task = host_project.update(host_cx, |project, cx| {
1305+// project.start_debug_session(
1306+// dap::DebugAdapterConfig {
1307+// label: "test config".into(),
1308+// kind: dap::DebugAdapterKind::Fake,
1309+// request: dap::DebugRequestType::Launch,
1310+// program: None,
1311+// cwd: None,
1312+// initialize_args: None,
1313+// },
1314+// cx,
1315+// )
1316+// });
1317+
1318+// let (session, client) = task.await.unwrap();
1319+
1320+// client
1321+// .on_request::<Initialize, _>(move |_, _| {
1322+// Ok(dap::Capabilities {
1323+// supports_step_back: Some(true),
1324+// ..Default::default()
1325+// })
1326+// })
1327+// .await;
1328+
1329+// client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1330+
1331+// let stack_frames = vec![dap::StackFrame {
1332+// id: 1,
1333+// name: "Stack Frame 1".into(),
1334+// source: Some(dap::Source {
1335+// name: Some("test.js".into()),
1336+// path: Some("/project/src/test.js".into()),
1337+// source_reference: None,
1338+// presentation_hint: None,
1339+// origin: None,
1340+// sources: None,
1341+// adapter_data: None,
1342+// checksums: None,
1343+// }),
1344+// line: 1,
1345+// column: 1,
1346+// end_line: None,
1347+// end_column: None,
1348+// can_restart: None,
1349+// instruction_pointer_reference: None,
1350+// module_id: None,
1351+// presentation_hint: None,
1352+// }];
1353+
1354+// let scopes = vec![Scope {
1355+// name: "Scope 1".into(),
1356+// presentation_hint: None,
1357+// variables_reference: 1,
1358+// named_variables: None,
1359+// indexed_variables: None,
1360+// expensive: false,
1361+// source: None,
1362+// line: None,
1363+// column: None,
1364+// end_line: None,
1365+// end_column: None,
1366+// }];
1367+
1368+// let variable_1 = Variable {
1369+// name: "variable 1".into(),
1370+// value: "1".into(),
1371+// type_: None,
1372+// presentation_hint: None,
1373+// evaluate_name: None,
1374+// variables_reference: 2,
1375+// named_variables: None,
1376+// indexed_variables: None,
1377+// memory_reference: None,
1378+// };
1379+
1380+// let variable_2 = Variable {
1381+// name: "variable 2".into(),
1382+// value: "2".into(),
1383+// type_: None,
1384+// presentation_hint: None,
1385+// evaluate_name: None,
1386+// variables_reference: 3,
1387+// named_variables: None,
1388+// indexed_variables: None,
1389+// memory_reference: None,
1390+// };
1391+
1392+// let variable_3 = Variable {
1393+// name: "variable 3".into(),
1394+// value: "hello world".into(),
1395+// type_: None,
1396+// presentation_hint: None,
1397+// evaluate_name: None,
1398+// variables_reference: 4,
1399+// named_variables: None,
1400+// indexed_variables: None,
1401+// memory_reference: None,
1402+// };
1403+
1404+// let variable_4 = Variable {
1405+// name: "variable 4".into(),
1406+// value: "hello world this is the final variable".into(),
1407+// type_: None,
1408+// presentation_hint: None,
1409+// evaluate_name: None,
1410+// variables_reference: 0,
1411+// named_variables: None,
1412+// indexed_variables: None,
1413+// memory_reference: None,
1414+// };
1415+
1416+// client
1417+// .on_request::<StackTrace, _>({
1418+// let stack_frames = std::sync::Arc::new(stack_frames.clone());
1419+// move |_, args| {
1420+// assert_eq!(1, args.thread_id);
1421+
1422+// Ok(dap::StackTraceResponse {
1423+// stack_frames: (*stack_frames).clone(),
1424+// total_frames: None,
1425+// })
1426+// }
1427+// })
1428+// .await;
1429+
1430+// client
1431+// .on_request::<Scopes, _>({
1432+// let scopes = Arc::new(scopes.clone());
1433+// move |_, args| {
1434+// assert_eq!(1, args.frame_id);
1435+
1436+// Ok(dap::ScopesResponse {
1437+// scopes: (*scopes).clone(),
1438+// })
1439+// }
1440+// })
1441+// .await;
1442+
1443+// let first_variable_request = vec![variable_1.clone(), variable_2.clone()];
1444+
1445+// client
1446+// .on_request::<Variables, _>({
1447+// move |_, args| {
1448+// assert_eq!(1, args.variables_reference);
1449+
1450+// Ok(dap::VariablesResponse {
1451+// variables: first_variable_request.clone(),
1452+// })
1453+// }
1454+// })
1455+// .await;
1456+
1457+// client
1458+// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1459+// reason: dap::StoppedEventReason::Pause,
1460+// description: None,
1461+// thread_id: Some(1),
1462+// preserve_focus_hint: None,
1463+// text: None,
1464+// all_threads_stopped: None,
1465+// hit_breakpoint_ids: None,
1466+// }))
1467+// .await;
1468+
1469+// host_cx.run_until_parked();
1470+// remote_cx.run_until_parked();
1471+
1472+// let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
1473+// let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1474+// let active_debug_panel_item = debug_panel
1475+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
1476+// .unwrap();
1477+
1478+// assert_eq!(
1479+// 1,
1480+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
1481+// );
1482+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
1483+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
1484+// active_debug_panel_item
1485+// });
1486+
1487+// let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
1488+// let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1489+// let active_debug_panel_item = debug_panel
1490+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
1491+// .unwrap();
1492+
1493+// assert_eq!(
1494+// 1,
1495+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
1496+// );
1497+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
1498+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
1499+// active_debug_panel_item
1500+// });
1501+
1502+// let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"];
1503+// let first_variable_containers = vec![
1504+// VariableContainer {
1505+// container_reference: scopes[0].variables_reference,
1506+// variable: variable_1.clone(),
1507+// depth: 1,
1508+// },
1509+// VariableContainer {
1510+// container_reference: scopes[0].variables_reference,
1511+// variable: variable_2.clone(),
1512+// depth: 1,
1513+// },
1514+// ];
1515+
1516+// local_debug_item
1517+// .update(host_cx, |this, _| this.variable_list().clone())
1518+// .update(host_cx, |variable_list, cx| {
1519+// assert_eq!(1, variable_list.scopes().len());
1520+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1521+// assert_eq!(&first_variable_containers, &variable_list.variables());
1522+
1523+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
1524+// });
1525+
1526+// client
1527+// .on_request::<Variables, _>({
1528+// let variables = Arc::new(vec![variable_3.clone()]);
1529+// move |_, args| {
1530+// assert_eq!(2, args.variables_reference);
1531+
1532+// Ok(dap::VariablesResponse {
1533+// variables: (*variables).clone(),
1534+// })
1535+// }
1536+// })
1537+// .await;
1538+
1539+// remote_debug_item
1540+// .update(remote_cx, |this, _| this.variable_list().clone())
1541+// .update(remote_cx, |variable_list, cx| {
1542+// assert_eq!(1, variable_list.scopes().len());
1543+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1544+// assert_eq!(&first_variable_containers, &variable_list.variables());
1545+
1546+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
1547+
1548+// variable_list.toggle_variable(&scopes[0], &variable_1, 1, cx);
1549+// });
1550+
1551+// host_cx.run_until_parked();
1552+// remote_cx.run_until_parked();
1553+
1554+// let second_req_variable_list = vec![
1555+// VariableContainer {
1556+// container_reference: scopes[0].variables_reference,
1557+// variable: variable_1.clone(),
1558+// depth: 1,
1559+// },
1560+// VariableContainer {
1561+// container_reference: variable_1.variables_reference,
1562+// variable: variable_3.clone(),
1563+// depth: 2,
1564+// },
1565+// VariableContainer {
1566+// container_reference: scopes[0].variables_reference,
1567+// variable: variable_2.clone(),
1568+// depth: 1,
1569+// },
1570+// ];
1571+
1572+// remote_debug_item
1573+// .update(remote_cx, |this, _| this.variable_list().clone())
1574+// .update(remote_cx, |variable_list, cx| {
1575+// assert_eq!(1, variable_list.scopes().len());
1576+// assert_eq!(3, variable_list.variables().len());
1577+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1578+// assert_eq!(&second_req_variable_list, &variable_list.variables());
1579+
1580+// variable_list.assert_visual_entries(
1581+// vec![
1582+// "v Scope 1",
1583+// " v variable 1",
1584+// " > variable 3",
1585+// " > variable 2",
1586+// ],
1587+// cx,
1588+// );
1589+// });
1590+
1591+// client
1592+// .on_request::<Variables, _>({
1593+// let variables = Arc::new(vec![variable_4.clone()]);
1594+// move |_, args| {
1595+// assert_eq!(3, args.variables_reference);
1596+
1597+// Ok(dap::VariablesResponse {
1598+// variables: (*variables).clone(),
1599+// })
1600+// }
1601+// })
1602+// .await;
1603+
1604+// local_debug_item
1605+// .update(host_cx, |this, _| this.variable_list().clone())
1606+// .update(host_cx, |variable_list, cx| {
1607+// assert_eq!(1, variable_list.scopes().len());
1608+// assert_eq!(3, variable_list.variables().len());
1609+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1610+// assert_eq!(&second_req_variable_list, &variable_list.variables());
1611+
1612+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
1613+
1614+// variable_list.toggle_variable(&scopes[0], &variable_2.clone(), 1, cx);
1615+// });
1616+
1617+// host_cx.run_until_parked();
1618+// remote_cx.run_until_parked();
1619+
1620+// let final_variable_containers: Vec<VariableContainer> = vec![
1621+// VariableContainer {
1622+// container_reference: scopes[0].variables_reference,
1623+// variable: variable_1.clone(),
1624+// depth: 1,
1625+// },
1626+// VariableContainer {
1627+// container_reference: variable_1.variables_reference,
1628+// variable: variable_3.clone(),
1629+// depth: 2,
1630+// },
1631+// VariableContainer {
1632+// container_reference: scopes[0].variables_reference,
1633+// variable: variable_2.clone(),
1634+// depth: 1,
1635+// },
1636+// VariableContainer {
1637+// container_reference: variable_2.variables_reference,
1638+// variable: variable_4.clone(),
1639+// depth: 2,
1640+// },
1641+// ];
1642+
1643+// remote_debug_item
1644+// .update(remote_cx, |this, _| this.variable_list().clone())
1645+// .update(remote_cx, |variable_list, cx| {
1646+// assert_eq!(1, variable_list.scopes().len());
1647+// assert_eq!(4, variable_list.variables().len());
1648+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1649+// assert_eq!(&final_variable_containers, &variable_list.variables());
1650+
1651+// variable_list.assert_visual_entries(
1652+// vec![
1653+// "v Scope 1",
1654+// " v variable 1",
1655+// " > variable 3",
1656+// " > variable 2",
1657+// ],
1658+// cx,
1659+// );
1660+// });
1661+
1662+// local_debug_item
1663+// .update(host_cx, |this, _| this.variable_list().clone())
1664+// .update(host_cx, |variable_list, cx| {
1665+// assert_eq!(1, variable_list.scopes().len());
1666+// assert_eq!(4, variable_list.variables().len());
1667+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1668+// assert_eq!(&final_variable_containers, &variable_list.variables());
1669+
1670+// variable_list.assert_visual_entries(
1671+// vec![
1672+// "v Scope 1",
1673+// " > variable 1",
1674+// " v variable 2",
1675+// " > variable 4",
1676+// ],
1677+// cx,
1678+// );
1679+// });
1680+
1681+// late_join_zed.join_project(host_project_id).await;
1682+// let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) =
1683+// late_join_zed.expand().await;
1684+
1685+// late_join_cx.run_until_parked();
1686+
1687+// let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| {
1688+// let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1689+// let active_debug_panel_item = debug_panel
1690+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
1691+// .unwrap();
1692+
1693+// assert_eq!(
1694+// 1,
1695+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
1696+// );
1697+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
1698+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
1699+// active_debug_panel_item
1700+// });
1701+
1702+// last_join_remote_item
1703+// .update(late_join_cx, |this, _| this.variable_list().clone())
1704+// .update(late_join_cx, |variable_list, cx| {
1705+// assert_eq!(1, variable_list.scopes().len());
1706+// assert_eq!(4, variable_list.variables().len());
1707+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
1708+// assert_eq!(final_variable_containers, variable_list.variables());
1709+
1710+// variable_list.assert_visual_entries(first_visual_entries, cx);
1711+// });
1712+
1713+// let shutdown_client = host_project.update(host_cx, |project, cx| {
1714+// project.dap_store().update(cx, |dap_store, cx| {
1715+// dap_store.shutdown_session(&session.read(cx).id(), cx)
1716+// })
1717+// });
1718+
1719+// shutdown_client.await.unwrap();
1720+// }
1721+
1722+#[gpui::test]
1723+async fn test_ignore_breakpoints(
1724+ _host_cx: &mut TestAppContext,
1725+ _remote_cx: &mut TestAppContext,
1726+ _cx_c: &mut TestAppContext,
1727+) {
1728+ unimplemented!("Collab is still being refactored");
1729+ // let executor = host_cx.executor();
1730+ // let mut server = TestServer::start(executor).await;
1731+
1732+ // let (mut host_zed, mut remote_zed, mut late_join_zed) =
1733+ // setup_three_member_test(&mut server, host_cx, remote_cx, cx_c).await;
1734+
1735+ // let (host_project_id, worktree_id) = host_zed
1736+ // .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
1737+ // .await;
1738+
1739+ // remote_zed.join_project(host_project_id).await;
1740+
1741+ // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
1742+ // let (_client_remote, remote_workspace, remote_project, remote_cx) = remote_zed.expand().await;
1743+
1744+ // let project_path = ProjectPath {
1745+ // worktree_id,
1746+ // path: Arc::from(Path::new(&"test.txt")),
1747+ // };
1748+
1749+ // let local_editor = host_workspace
1750+ // .update_in(host_cx, |workspace, window, cx| {
1751+ // workspace.open_path(project_path.clone(), None, true, window, cx)
1752+ // })
1753+ // .await
1754+ // .unwrap()
1755+ // .downcast::<Editor>()
1756+ // .unwrap();
1757+
1758+ // local_editor.update_in(host_cx, |editor, window, cx| {
1759+ // editor.move_down(&editor::actions::MoveDown, window, cx);
1760+ // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); // Line 2
1761+ // editor.move_down(&editor::actions::MoveDown, window, cx);
1762+ // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
1763+ // // Line 3
1764+ // });
1765+
1766+ // host_cx.run_until_parked();
1767+ // remote_cx.run_until_parked();
1768+
1769+ // let task = host_project.update(host_cx, |project, cx| {
1770+ // project.start_debug_session(dap::test_config(None), cx)
1771+ // });
1772+
1773+ // let session = task.await.unwrap();
1774+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
1775+ // let client_id = client.id();
1776+
1777+ // client
1778+ // .on_request::<Initialize, _>(move |_, _| {
1779+ // Ok(dap::Capabilities {
1780+ // supports_configuration_done_request: Some(true),
1781+ // ..Default::default()
1782+ // })
1783+ // })
1784+ // .await;
1785+
1786+ // let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1787+ // client
1788+ // .on_request::<SetBreakpoints, _>({
1789+ // let called_set_breakpoints = called_set_breakpoints.clone();
1790+ // move |_, args| {
1791+ // assert_eq!("/project/test.txt", args.source.path.unwrap());
1792+
1793+ // let mut actual_breakpoints = args.breakpoints.unwrap();
1794+ // actual_breakpoints.sort_by_key(|b| b.line);
1795+
1796+ // let expected_breakpoints = vec![
1797+ // SourceBreakpoint {
1798+ // line: 2,
1799+ // column: None,
1800+ // condition: None,
1801+ // hit_condition: None,
1802+ // log_message: None,
1803+ // mode: None,
1804+ // },
1805+ // SourceBreakpoint {
1806+ // line: 3,
1807+ // column: None,
1808+ // condition: None,
1809+ // hit_condition: None,
1810+ // log_message: None,
1811+ // mode: None,
1812+ // },
1813+ // ];
1814+
1815+ // assert_eq!(actual_breakpoints, expected_breakpoints);
1816+
1817+ // called_set_breakpoints.store(true, Ordering::SeqCst);
1818+
1819+ // Ok(dap::SetBreakpointsResponse {
1820+ // breakpoints: Vec::default(),
1821+ // })
1822+ // }
1823+ // })
1824+ // .await;
1825+
1826+ // client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1827+ // client
1828+ // .on_request::<StackTrace, _>(move |_, _| {
1829+ // Ok(dap::StackTraceResponse {
1830+ // stack_frames: Vec::default(),
1831+ // total_frames: None,
1832+ // })
1833+ // })
1834+ // .await;
1835+
1836+ // client
1837+ // .fake_event(dap::messages::Events::Initialized(Some(
1838+ // dap::Capabilities {
1839+ // supports_configuration_done_request: Some(true),
1840+ // ..Default::default()
1841+ // },
1842+ // )))
1843+ // .await;
1844+
1845+ // host_cx.run_until_parked();
1846+ // remote_cx.run_until_parked();
1847+
1848+ // assert!(
1849+ // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1850+ // "SetBreakpoint request must be called when starting debug session"
1851+ // );
1852+
1853+ // client
1854+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1855+ // reason: dap::StoppedEventReason::Pause,
1856+ // description: None,
1857+ // thread_id: Some(1),
1858+ // preserve_focus_hint: None,
1859+ // text: None,
1860+ // all_threads_stopped: None,
1861+ // hit_breakpoint_ids: None,
1862+ // }))
1863+ // .await;
1864+
1865+ // host_cx.run_until_parked();
1866+ // remote_cx.run_until_parked();
1867+
1868+ // let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
1869+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1870+ // let active_session = debug_panel
1871+ // .update(cx, |this, cx| this.active_session(cx))
1872+ // .unwrap();
1873+
1874+ // assert_eq!(
1875+ // 1,
1876+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
1877+ // );
1878+
1879+ // let session_id = debug_panel.update(cx, |this, cx| {
1880+ // this.dap_store()
1881+ // .read(cx)
1882+ // .session_by_client_id(client.id())
1883+ // .unwrap()
1884+ // .read(cx)
1885+ // .id()
1886+ // });
1887+
1888+ // let breakpoints_ignored = active_session.read(cx).are_breakpoints_ignored(cx);
1889+
1890+ // assert_eq!(session_id, active_session.read(cx).session().read(cx).id());
1891+ // assert_eq!(false, breakpoints_ignored);
1892+ // assert_eq!(client.id(), active_session.read(cx).client_id());
1893+ // assert_eq!(1, active_session.read(cx).thread_id().0);
1894+ // active_session
1895+ // });
1896+
1897+ // called_set_breakpoints.store(false, Ordering::SeqCst);
1898+
1899+ // client
1900+ // .on_request::<SetBreakpoints, _>({
1901+ // let called_set_breakpoints = called_set_breakpoints.clone();
1902+ // move |_, args| {
1903+ // assert_eq!("/project/test.txt", args.source.path.unwrap());
1904+ // assert_eq!(args.breakpoints, Some(vec![]));
1905+
1906+ // called_set_breakpoints.store(true, Ordering::SeqCst);
1907+
1908+ // Ok(dap::SetBreakpointsResponse {
1909+ // breakpoints: Vec::default(),
1910+ // })
1911+ // }
1912+ // })
1913+ // .await;
1914+
1915+ // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
1916+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
1917+ // let active_session = debug_panel
1918+ // .update(cx, |this, cx| this.active_session(cx))
1919+ // .unwrap();
1920+
1921+ // assert_eq!(
1922+ // 1,
1923+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
1924+ // );
1925+
1926+ // assert_eq!(false, active_session.read(cx).are_breakpoints_ignored(cx));
1927+ // assert_eq!(client.id(), active_session.read(cx).client_id());
1928+ // assert_eq!(1, active_session.read(cx).thread_id().0);
1929+
1930+ // active_session
1931+ // });
1932+
1933+ // local_debug_item.update(host_cx, |item, cx| {
1934+ // item.toggle_ignore_breakpoints(cx); // Set to true
1935+ // assert_eq!(true, item.are_breakpoints_ignored(cx));
1936+ // });
1937+
1938+ // host_cx.run_until_parked();
1939+ // remote_cx.run_until_parked();
1940+
1941+ // assert!(
1942+ // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1943+ // "SetBreakpoint request must be called to ignore breakpoints"
1944+ // );
1945+
1946+ // client
1947+ // .on_request::<SetBreakpoints, _>({
1948+ // let called_set_breakpoints = called_set_breakpoints.clone();
1949+ // move |_, _args| {
1950+ // called_set_breakpoints.store(true, Ordering::SeqCst);
1951+
1952+ // Ok(dap::SetBreakpointsResponse {
1953+ // breakpoints: Vec::default(),
1954+ // })
1955+ // }
1956+ // })
1957+ // .await;
1958+
1959+ // let remote_editor = remote_workspace
1960+ // .update_in(remote_cx, |workspace, window, cx| {
1961+ // workspace.open_path(project_path.clone(), None, true, window, cx)
1962+ // })
1963+ // .await
1964+ // .unwrap()
1965+ // .downcast::<Editor>()
1966+ // .unwrap();
1967+
1968+ // called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst);
1969+
1970+ // remote_editor.update_in(remote_cx, |editor, window, cx| {
1971+ // // Line 1
1972+ // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
1973+ // });
1974+
1975+ // host_cx.run_until_parked();
1976+ // remote_cx.run_until_parked();
1977+
1978+ // assert!(
1979+ // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1980+ // "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints"
1981+ // );
1982+
1983+ // remote_debug_item.update(remote_cx, |debug_panel, cx| {
1984+ // let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx);
1985+
1986+ // assert_eq!(true, breakpoints_ignored);
1987+ // assert_eq!(client.id(), debug_panel.client_id());
1988+ // assert_eq!(1, debug_panel.thread_id().0);
1989+ // });
1990+
1991+ // client
1992+ // .on_request::<SetBreakpoints, _>({
1993+ // let called_set_breakpoints = called_set_breakpoints.clone();
1994+ // move |_, args| {
1995+ // assert_eq!("/project/test.txt", args.source.path.unwrap());
1996+
1997+ // let mut actual_breakpoints = args.breakpoints.unwrap();
1998+ // actual_breakpoints.sort_by_key(|b| b.line);
1999+
2000+ // let expected_breakpoints = vec![
2001+ // SourceBreakpoint {
2002+ // line: 1,
2003+ // column: None,
2004+ // condition: None,
2005+ // hit_condition: None,
2006+ // log_message: None,
2007+ // mode: None,
2008+ // },
2009+ // SourceBreakpoint {
2010+ // line: 2,
2011+ // column: None,
2012+ // condition: None,
2013+ // hit_condition: None,
2014+ // log_message: None,
2015+ // mode: None,
2016+ // },
2017+ // SourceBreakpoint {
2018+ // line: 3,
2019+ // column: None,
2020+ // condition: None,
2021+ // hit_condition: None,
2022+ // log_message: None,
2023+ // mode: None,
2024+ // },
2025+ // ];
2026+
2027+ // assert_eq!(actual_breakpoints, expected_breakpoints);
2028+
2029+ // called_set_breakpoints.store(true, Ordering::SeqCst);
2030+
2031+ // Ok(dap::SetBreakpointsResponse {
2032+ // breakpoints: Vec::default(),
2033+ // })
2034+ // }
2035+ // })
2036+ // .await;
2037+
2038+ // late_join_zed.join_project(host_project_id).await;
2039+ // let (_late_join_client, late_join_workspace, late_join_project, late_join_cx) =
2040+ // late_join_zed.expand().await;
2041+
2042+ // late_join_cx.run_until_parked();
2043+
2044+ // let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| {
2045+ // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
2046+ // let active_session = debug_panel
2047+ // .update(cx, |this, cx| this.active_session(cx))
2048+ // .unwrap();
2049+
2050+ // let breakpoints_ignored = active_session.read(cx).are_breakpoints_ignored(cx);
2051+
2052+ // assert_eq!(true, breakpoints_ignored);
2053+
2054+ // assert_eq!(
2055+ // 1,
2056+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
2057+ // );
2058+ // assert_eq!(client.id(), active_session.read(cx).client_id());
2059+ // assert_eq!(1, active_session.read(cx).thread_id().0);
2060+ // active_session
2061+ // });
2062+
2063+ // remote_debug_item.update(remote_cx, |item, cx| {
2064+ // item.toggle_ignore_breakpoints(cx);
2065+ // });
2066+
2067+ // host_cx.run_until_parked();
2068+ // remote_cx.run_until_parked();
2069+ // late_join_cx.run_until_parked();
2070+
2071+ // assert!(
2072+ // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
2073+ // "SetBreakpoint request should be called to update breakpoints"
2074+ // );
2075+
2076+ // client
2077+ // .on_request::<SetBreakpoints, _>({
2078+ // let called_set_breakpoints = called_set_breakpoints.clone();
2079+ // move |_, args| {
2080+ // assert_eq!("/project/test.txt", args.source.path.unwrap());
2081+ // assert_eq!(args.breakpoints, Some(vec![]));
2082+
2083+ // called_set_breakpoints.store(true, Ordering::SeqCst);
2084+
2085+ // Ok(dap::SetBreakpointsResponse {
2086+ // breakpoints: Vec::default(),
2087+ // })
2088+ // }
2089+ // })
2090+ // .await;
2091+
2092+ // local_debug_item.update(host_cx, |debug_panel_item, cx| {
2093+ // assert_eq!(
2094+ // false,
2095+ // debug_panel_item.are_breakpoints_ignored(cx),
2096+ // "Remote client set this to false"
2097+ // );
2098+ // });
2099+
2100+ // remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
2101+ // assert_eq!(
2102+ // false,
2103+ // debug_panel_item.are_breakpoints_ignored(cx),
2104+ // "Remote client set this to false"
2105+ // );
2106+ // });
2107+
2108+ // last_join_remote_item.update(late_join_cx, |debug_panel_item, cx| {
2109+ // assert_eq!(
2110+ // false,
2111+ // debug_panel_item.are_breakpoints_ignored(cx),
2112+ // "Remote client set this to false"
2113+ // );
2114+ // });
2115+
2116+ // let shutdown_client = host_project.update(host_cx, |project, cx| {
2117+ // project.dap_store().update(cx, |dap_store, cx| {
2118+ // dap_store.shutdown_session(&session.read(cx).id(), cx)
2119+ // })
2120+ // });
2121+
2122+ // shutdown_client.await.unwrap();
2123+
2124+ // host_cx.run_until_parked();
2125+ // remote_cx.run_until_parked();
2126+
2127+ // remote_project.update(remote_cx, |project, cx| {
2128+ // project.dap_store().update(cx, |dap_store, _cx| {
2129+ // let sessions = dap_store.sessions().collect::<Vec<_>>();
2130+
2131+ // assert_eq!(
2132+ // None,
2133+ // dap_store.session_by_client_id(&client_id),
2134+ // "No client_id to session mapping should exist after shutdown"
2135+ // );
2136+ // assert_eq!(
2137+ // 0,
2138+ // sessions.len(),
2139+ // "No sessions should be left after shutdown"
2140+ // );
2141+ // })
2142+ // });
2143+
2144+ // late_join_project.update(late_join_cx, |project, cx| {
2145+ // project.dap_store().update(cx, |dap_store, _cx| {
2146+ // let sessions = dap_store.sessions().collect::<Vec<_>>();
2147+
2148+ // assert_eq!(
2149+ // None,
2150+ // dap_store.session_by_client_id(&client_id),
2151+ // "No client_id to session mapping should exist after shutdown"
2152+ // );
2153+ // assert_eq!(
2154+ // 0,
2155+ // sessions.len(),
2156+ // "No sessions should be left after shutdown"
2157+ // );
2158+ // })
2159+ // });
2160+}
2161+
2162+#[gpui::test]
2163+async fn test_debug_panel_console(_host_cx: &mut TestAppContext, _remote_cx: &mut TestAppContext) {
2164+ unimplemented!("Collab is still being refactored");
2165+ // let executor = host_cx.executor();
2166+ // let mut server = TestServer::start(executor).await;
2167+
2168+ // let (mut host_zed, mut remote_zed) =
2169+ // setup_two_member_test(&mut server, host_cx, remote_cx).await;
2170+
2171+ // let (host_project_id, _) = host_zed.host_project(None).await;
2172+ // remote_zed.join_project(host_project_id).await;
2173+
2174+ // let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
2175+ // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
2176+
2177+ // remote_cx.run_until_parked();
2178+
2179+ // let task = host_project.update(host_cx, |project, cx| {
2180+ // project.start_debug_session(dap::test_config(None), cx)
2181+ // });
2182+
2183+ // let session = task.await.unwrap();
2184+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap());
2185+
2186+ // client
2187+ // .on_request::<Initialize, _>(move |_, _| {
2188+ // Ok(dap::Capabilities {
2189+ // supports_step_back: Some(false),
2190+ // ..Default::default()
2191+ // })
2192+ // })
2193+ // .await;
2194+
2195+ // client.on_request::<Launch, _>(move |_, _| Ok(())).await;
2196+
2197+ // client
2198+ // .on_request::<StackTrace, _>(move |_, _| {
2199+ // Ok(dap::StackTraceResponse {
2200+ // stack_frames: Vec::default(),
2201+ // total_frames: None,
2202+ // })
2203+ // })
2204+ // .await;
2205+
2206+ // client
2207+ // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2208+ // reason: dap::StoppedEventReason::Pause,
2209+ // description: None,
2210+ // thread_id: Some(1),
2211+ // preserve_focus_hint: None,
2212+ // text: None,
2213+ // all_threads_stopped: None,
2214+ // hit_breakpoint_ids: None,
2215+ // }))
2216+ // .await;
2217+
2218+ // client
2219+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2220+ // category: None,
2221+ // output: "First line".to_string(),
2222+ // data: None,
2223+ // variables_reference: None,
2224+ // source: None,
2225+ // line: None,
2226+ // column: None,
2227+ // group: None,
2228+ // location_reference: None,
2229+ // }))
2230+ // .await;
2231+
2232+ // client
2233+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2234+ // category: Some(dap::OutputEventCategory::Stdout),
2235+ // output: "First group".to_string(),
2236+ // data: None,
2237+ // variables_reference: None,
2238+ // source: None,
2239+ // line: None,
2240+ // column: None,
2241+ // group: Some(dap::OutputEventGroup::Start),
2242+ // location_reference: None,
2243+ // }))
2244+ // .await;
2245+
2246+ // client
2247+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2248+ // category: Some(dap::OutputEventCategory::Stdout),
2249+ // output: "First item in group 1".to_string(),
2250+ // data: None,
2251+ // variables_reference: None,
2252+ // source: None,
2253+ // line: None,
2254+ // column: None,
2255+ // group: None,
2256+ // location_reference: None,
2257+ // }))
2258+ // .await;
2259+
2260+ // client
2261+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2262+ // category: Some(dap::OutputEventCategory::Stdout),
2263+ // output: "Second item in group 1".to_string(),
2264+ // data: None,
2265+ // variables_reference: None,
2266+ // source: None,
2267+ // line: None,
2268+ // column: None,
2269+ // group: None,
2270+ // location_reference: None,
2271+ // }))
2272+ // .await;
2273+
2274+ // client
2275+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2276+ // category: Some(dap::OutputEventCategory::Stdout),
2277+ // output: "Second group".to_string(),
2278+ // data: None,
2279+ // variables_reference: None,
2280+ // source: None,
2281+ // line: None,
2282+ // column: None,
2283+ // group: Some(dap::OutputEventGroup::Start),
2284+ // location_reference: None,
2285+ // }))
2286+ // .await;
2287+
2288+ // client
2289+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2290+ // category: Some(dap::OutputEventCategory::Stdout),
2291+ // output: "First item in group 2".to_string(),
2292+ // data: None,
2293+ // variables_reference: None,
2294+ // source: None,
2295+ // line: None,
2296+ // column: None,
2297+ // group: None,
2298+ // location_reference: None,
2299+ // }))
2300+ // .await;
2301+
2302+ // client
2303+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2304+ // category: Some(dap::OutputEventCategory::Stdout),
2305+ // output: "Second item in group 2".to_string(),
2306+ // data: None,
2307+ // variables_reference: None,
2308+ // source: None,
2309+ // line: None,
2310+ // column: None,
2311+ // group: None,
2312+ // location_reference: None,
2313+ // }))
2314+ // .await;
2315+
2316+ // client
2317+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2318+ // category: Some(dap::OutputEventCategory::Stdout),
2319+ // output: "End group 2".to_string(),
2320+ // data: None,
2321+ // variables_reference: None,
2322+ // source: None,
2323+ // line: None,
2324+ // column: None,
2325+ // group: Some(dap::OutputEventGroup::End),
2326+ // location_reference: None,
2327+ // }))
2328+ // .await;
2329+
2330+ // client
2331+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2332+ // category: Some(dap::OutputEventCategory::Stdout),
2333+ // output: "Third group".to_string(),
2334+ // data: None,
2335+ // variables_reference: None,
2336+ // source: None,
2337+ // line: None,
2338+ // column: None,
2339+ // group: Some(dap::OutputEventGroup::StartCollapsed),
2340+ // location_reference: None,
2341+ // }))
2342+ // .await;
2343+
2344+ // client
2345+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2346+ // category: Some(dap::OutputEventCategory::Stdout),
2347+ // output: "First item in group 3".to_string(),
2348+ // data: None,
2349+ // variables_reference: None,
2350+ // source: None,
2351+ // line: None,
2352+ // column: None,
2353+ // group: None,
2354+ // location_reference: None,
2355+ // }))
2356+ // .await;
2357+
2358+ // client
2359+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2360+ // category: Some(dap::OutputEventCategory::Stdout),
2361+ // output: "Second item in group 3".to_string(),
2362+ // data: None,
2363+ // variables_reference: None,
2364+ // source: None,
2365+ // line: None,
2366+ // column: None,
2367+ // group: None,
2368+ // location_reference: None,
2369+ // }))
2370+ // .await;
2371+
2372+ // client
2373+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2374+ // category: Some(dap::OutputEventCategory::Stdout),
2375+ // output: "End group 3".to_string(),
2376+ // data: None,
2377+ // variables_reference: None,
2378+ // source: None,
2379+ // line: None,
2380+ // column: None,
2381+ // group: Some(dap::OutputEventGroup::End),
2382+ // location_reference: None,
2383+ // }))
2384+ // .await;
2385+
2386+ // client
2387+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2388+ // category: Some(dap::OutputEventCategory::Stdout),
2389+ // output: "Third item in group 1".to_string(),
2390+ // data: None,
2391+ // variables_reference: None,
2392+ // source: None,
2393+ // line: None,
2394+ // column: None,
2395+ // group: None,
2396+ // location_reference: None,
2397+ // }))
2398+ // .await;
2399+
2400+ // client
2401+ // .fake_event(dap::messages::Events::Output(dap::OutputEvent {
2402+ // category: Some(dap::OutputEventCategory::Stdout),
2403+ // output: "Second item".to_string(),
2404+ // data: None,
2405+ // variables_reference: None,
2406+ // source: None,
2407+ // line: None,
2408+ // column: None,
2409+ // group: Some(dap::OutputEventGroup::End),
2410+ // location_reference: None,
2411+ // }))
2412+ // .await;
2413+
2414+ // host_cx.run_until_parked();
2415+ // remote_cx.run_until_parked();
2416+
2417+ // active_session(remote_workspace, remote_cx).update(remote_cx, |session_item, cx| {
2418+ // session_item
2419+ // .mode()
2420+ // .as_running()
2421+ // .unwrap()
2422+ // .read(cx)
2423+ // .console()
2424+ // .update(cx, |console, cx| {
2425+ // console.editor().update(cx, |editor, cx| {
2426+ // pretty_assertions::assert_eq!(
2427+ // "
2428+ // <First line
2429+ // First group
2430+ // First item in group 1
2431+ // Second item in group 1
2432+ // Second group
2433+ // First item in group 2
2434+ // Second item in group 2
2435+ // End group 2
2436+ // ⋯ End group 3
2437+ // Third item in group 1
2438+ // Second item
2439+ // "
2440+ // .unindent(),
2441+ // editor.display_text(cx)
2442+ // );
2443+ // })
2444+ // });
2445+ // });
2446+
2447+ // let shutdown_client = host_project.update(host_cx, |project, cx| {
2448+ // project.dap_store().update(cx, |dap_store, cx| {
2449+ // dap_store.shutdown_session(&session.read(cx).session_id(), cx)
2450+ // })
2451+ // });
2452+
2453+ // shutdown_client.await.unwrap();
2454+}
crates/collab/src/tests/editor_tests.rs+204−1
@@ -21,7 +21,7 @@ use language::{ · 20 unmodified lines
2121 };
2222 use project::{
2323 project_settings::{InlineBlameSettings, ProjectSettings},
24− SERVER_PROGRESS_THROTTLE_TIMEOUT,
24+ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
2525 };
2626 use recent_projects::disconnected_overlay::DisconnectedOverlay;
2727 use rpc::RECEIVE_TIMEOUT;
@@ -2408,6 +2408,209 @@ fn main() { let foo = other::foo(); }"}; · 2380 unmodified lines
24082408 );
24092409 }
24102410
2411+#[gpui::test]
2412+async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2413+ let executor = cx_a.executor();
2414+ let mut server = TestServer::start(executor.clone()).await;
2415+ let client_a = server.create_client(cx_a, "user_a").await;
2416+ let client_b = server.create_client(cx_b, "user_b").await;
2417+ server
2418+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2419+ .await;
2420+ let active_call_a = cx_a.read(ActiveCall::global);
2421+ let active_call_b = cx_b.read(ActiveCall::global);
2422+ cx_a.update(editor::init);
2423+ cx_b.update(editor::init);
2424+ client_a
2425+ .fs()
2426+ .insert_tree(
2427+ "/a",
2428+ json!({
2429+ "test.txt": "one\ntwo\nthree\nfour\nfive",
2430+ }),
2431+ )
2432+ .await;
2433+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2434+ let project_path = ProjectPath {
2435+ worktree_id,
2436+ path: Arc::from(Path::new(&"test.txt")),
2437+ };
2438+ let abs_path = project_a.read_with(cx_a, |project, cx| {
2439+ project
2440+ .absolute_path(&project_path, cx)
2441+ .map(|path_buf| Arc::from(path_buf.to_owned()))
2442+ .unwrap()
2443+ });
2444+
2445+ active_call_a
2446+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2447+ .await
2448+ .unwrap();
2449+ let project_id = active_call_a
2450+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2451+ .await
2452+ .unwrap();
2453+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
2454+ active_call_b
2455+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2456+ .await
2457+ .unwrap();
2458+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2459+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2460+
2461+ // Client A opens an editor.
2462+ let editor_a = workspace_a
2463+ .update_in(cx_a, |workspace, window, cx| {
2464+ workspace.open_path(project_path.clone(), None, true, window, cx)
2465+ })
2466+ .await
2467+ .unwrap()
2468+ .downcast::<Editor>()
2469+ .unwrap();
2470+
2471+ // Client B opens same editor as A.
2472+ let editor_b = workspace_b
2473+ .update_in(cx_b, |workspace, window, cx| {
2474+ workspace.open_path(project_path.clone(), None, true, window, cx)
2475+ })
2476+ .await
2477+ .unwrap()
2478+ .downcast::<Editor>()
2479+ .unwrap();
2480+
2481+ cx_a.run_until_parked();
2482+ cx_b.run_until_parked();
2483+
2484+ // Client A adds breakpoint on line (1)
2485+ editor_a.update_in(cx_a, |editor, window, cx| {
2486+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2487+ });
2488+
2489+ cx_a.run_until_parked();
2490+ cx_b.run_until_parked();
2491+
2492+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2493+ editor
2494+ .breakpoint_store()
2495+ .clone()
2496+ .unwrap()
2497+ .read(cx)
2498+ .all_breakpoints(cx)
2499+ .clone()
2500+ });
2501+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2502+ editor
2503+ .breakpoint_store()
2504+ .clone()
2505+ .unwrap()
2506+ .read(cx)
2507+ .all_breakpoints(cx)
2508+ .clone()
2509+ });
2510+
2511+ assert_eq!(1, breakpoints_a.len());
2512+ assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2513+ assert_eq!(breakpoints_a, breakpoints_b);
2514+
2515+ // Client B adds breakpoint on line(2)
2516+ editor_b.update_in(cx_b, |editor, window, cx| {
2517+ editor.move_down(&editor::actions::MoveDown, window, cx);
2518+ editor.move_down(&editor::actions::MoveDown, window, cx);
2519+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2520+ });
2521+
2522+ cx_a.run_until_parked();
2523+ cx_b.run_until_parked();
2524+
2525+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2526+ editor
2527+ .breakpoint_store()
2528+ .clone()
2529+ .unwrap()
2530+ .read(cx)
2531+ .all_breakpoints(cx)
2532+ .clone()
2533+ });
2534+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2535+ editor
2536+ .breakpoint_store()
2537+ .clone()
2538+ .unwrap()
2539+ .read(cx)
2540+ .all_breakpoints(cx)
2541+ .clone()
2542+ });
2543+
2544+ assert_eq!(1, breakpoints_a.len());
2545+ assert_eq!(breakpoints_a, breakpoints_b);
2546+ assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
2547+
2548+ // Client A removes last added breakpoint from client B
2549+ editor_a.update_in(cx_a, |editor, window, cx| {
2550+ editor.move_down(&editor::actions::MoveDown, window, cx);
2551+ editor.move_down(&editor::actions::MoveDown, window, cx);
2552+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2553+ });
2554+
2555+ cx_a.run_until_parked();
2556+ cx_b.run_until_parked();
2557+
2558+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2559+ editor
2560+ .breakpoint_store()
2561+ .clone()
2562+ .unwrap()
2563+ .read(cx)
2564+ .all_breakpoints(cx)
2565+ .clone()
2566+ });
2567+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2568+ editor
2569+ .breakpoint_store()
2570+ .clone()
2571+ .unwrap()
2572+ .read(cx)
2573+ .all_breakpoints(cx)
2574+ .clone()
2575+ });
2576+
2577+ assert_eq!(1, breakpoints_a.len());
2578+ assert_eq!(breakpoints_a, breakpoints_b);
2579+ assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2580+
2581+ // Client B removes first added breakpoint by client A
2582+ editor_b.update_in(cx_b, |editor, window, cx| {
2583+ editor.move_up(&editor::actions::MoveUp, window, cx);
2584+ editor.move_up(&editor::actions::MoveUp, window, cx);
2585+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2586+ });
2587+
2588+ cx_a.run_until_parked();
2589+ cx_b.run_until_parked();
2590+
2591+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2592+ editor
2593+ .breakpoint_store()
2594+ .clone()
2595+ .unwrap()
2596+ .read(cx)
2597+ .all_breakpoints(cx)
2598+ .clone()
2599+ });
2600+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2601+ editor
2602+ .breakpoint_store()
2603+ .clone()
2604+ .unwrap()
2605+ .read(cx)
2606+ .all_breakpoints(cx)
2607+ .clone()
2608+ });
2609+
2610+ assert_eq!(0, breakpoints_a.len());
2611+ assert_eq!(breakpoints_a, breakpoints_b);
2612+}
2613+
24112614 #[track_caller]
24122615 fn tab_undo_assert(
24132616 cx_a: &mut EditorTestContext,
crates/dap/Cargo.toml+53−0
@@ -0,0 +1,53 @@
1+[package]
2+name = "dap"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+
8+[lints]
9+workspace = true
10+
11+[features]
12+test-support = [
13+ "gpui/test-support",
14+ "util/test-support",
15+ "task/test-support",
16+ "async-pipe",
17+ "settings/test-support",
18+]
19+
20+[dependencies]
21+anyhow.workspace = true
22+async-compression.workspace = true
23+async-pipe = { workspace = true, optional = true }
24+async-tar.workspace = true
25+async-trait.workspace = true
26+client.workspace = true
27+collections.workspace = true
28+dap-types.workspace = true
29+fs.workspace = true
30+futures.workspace = true
31+gpui.workspace = true
32+http_client.workspace = true
33+language.workspace = true
34+log.workspace = true
35+node_runtime.workspace = true
36+parking_lot.workspace = true
37+paths.workspace = true
38+schemars.workspace = true
39+serde.workspace = true
40+serde_json.workspace = true
41+settings.workspace = true
42+smallvec.workspace = true
43+smol.workspace = true
44+task.workspace = true
45+util.workspace = true
46+
47+[dev-dependencies]
48+async-pipe.workspace = true
49+env_logger.workspace = true
50+gpui = { workspace = true, features = ["test-support"] }
51+settings = { workspace = true, features = ["test-support"] }
52+task = { workspace = true, features = ["test-support"] }
53+util = { workspace = true, features = ["test-support"] }
crates/dap/LICENSE-GPL+1−0
@@ -0,0 +1 @@
1+../../LICENSE-GPL
crates/dap/docs/breakpoints.md+9−0
@@ -0,0 +1,9 @@
1+# Overview
2+
3+The active `Project` is responsible for maintain opened and closed breakpoints
4+as well as serializing breakpoints to save. At a high level project serializes
5+the positions of breakpoints that don't belong to any active buffers and handles
6+converting breakpoints from serializing to active whenever a buffer is opened/closed.
7+
8+`Project` also handles sending all relevant breakpoint information to debug adapter's
9+during debugging or when starting a debugger.
crates/dap/src/adapters.rs+370−0
@@ -0,0 +1,370 @@
1+use ::fs::Fs;
2+use anyhow::{anyhow, Context as _, Ok, Result};
3+use async_compression::futures::bufread::GzipDecoder;
4+use async_tar::Archive;
5+use async_trait::async_trait;
6+use futures::io::BufReader;
7+use gpui::{AsyncApp, SharedString};
8+pub use http_client::{github::latest_github_release, HttpClient};
9+use language::LanguageToolchainStore;
10+use node_runtime::NodeRuntime;
11+use serde::{Deserialize, Serialize};
12+use serde_json::Value;
13+use settings::WorktreeId;
14+use smol::{self, fs::File, lock::Mutex};
15+use std::{
16+ collections::{HashMap, HashSet},
17+ ffi::{OsStr, OsString},
18+ fmt::Debug,
19+ net::Ipv4Addr,
20+ ops::Deref,
21+ path::{Path, PathBuf},
22+ sync::Arc,
23+};
24+use task::DebugAdapterConfig;
25+use util::ResultExt;
26+
27+#[derive(Clone, Debug, PartialEq, Eq)]
28+pub enum DapStatus {
29+ None,
30+ CheckingForUpdate,
31+ Downloading,
32+ Failed { error: String },
33+}
34+
35+#[async_trait(?Send)]
36+pub trait DapDelegate {
37+ fn worktree_id(&self) -> WorktreeId;
38+ fn http_client(&self) -> Arc<dyn HttpClient>;
39+ fn node_runtime(&self) -> NodeRuntime;
40+ fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
41+ fn fs(&self) -> Arc<dyn Fs>;
42+ fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
43+ fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
44+ fn which(&self, command: &OsStr) -> Option<PathBuf>;
45+ async fn shell_env(&self) -> collections::HashMap<String, String>;
46+}
47+
48+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
49+pub struct DebugAdapterName(pub Arc<str>);
50+
51+impl Deref for DebugAdapterName {
52+ type Target = str;
53+
54+ fn deref(&self) -> &Self::Target {
55+ &self.0
56+ }
57+}
58+
59+impl AsRef<str> for DebugAdapterName {
60+ fn as_ref(&self) -> &str {
61+ &self.0
62+ }
63+}
64+
65+impl AsRef<Path> for DebugAdapterName {
66+ fn as_ref(&self) -> &Path {
67+ Path::new(&*self.0)
68+ }
69+}
70+
71+impl std::fmt::Display for DebugAdapterName {
72+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73+ std::fmt::Display::fmt(&self.0, f)
74+ }
75+}
76+
77+impl From<DebugAdapterName> for SharedString {
78+ fn from(name: DebugAdapterName) -> Self {
79+ SharedString::from(name.0)
80+ }
81+}
82+
83+impl<'a> From<&'a str> for DebugAdapterName {
84+ fn from(str: &'a str) -> DebugAdapterName {
85+ DebugAdapterName(str.to_string().into())
86+ }
87+}
88+
89+#[derive(Debug, Clone)]
90+pub struct TcpArguments {
91+ pub host: Ipv4Addr,
92+ pub port: u16,
93+ pub timeout: Option<u64>,
94+}
95+#[derive(Debug, Clone)]
96+pub struct DebugAdapterBinary {
97+ pub command: String,
98+ pub arguments: Option<Vec<OsString>>,
99+ pub envs: Option<HashMap<String, String>>,
100+ pub cwd: Option<PathBuf>,
101+ pub connection: Option<TcpArguments>,
102+}
103+
104+pub struct AdapterVersion {
105+ pub tag_name: String,
106+ pub url: String,
107+}
108+
109+pub enum DownloadedFileType {
110+ Vsix,
111+ GzipTar,
112+ Zip,
113+}
114+
115+pub struct GithubRepo {
116+ pub repo_name: String,
117+ pub repo_owner: String,
118+}
119+
120+pub async fn download_adapter_from_github(
121+ adapter_name: DebugAdapterName,
122+ github_version: AdapterVersion,
123+ file_type: DownloadedFileType,
124+ delegate: &dyn DapDelegate,
125+) -> Result<PathBuf> {
126+ let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
127+ let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
128+ let fs = delegate.fs();
129+
130+ if version_path.exists() {
131+ return Ok(version_path);
132+ }
133+
134+ if !adapter_path.exists() {
135+ fs.create_dir(&adapter_path.as_path())
136+ .await
137+ .context("Failed creating adapter path")?;
138+ }
139+
140+ log::debug!(
141+ "Downloading adapter {} from {}",
142+ adapter_name,
143+ &github_version.url,
144+ );
145+
146+ let mut response = delegate
147+ .http_client()
148+ .get(&github_version.url, Default::default(), true)
149+ .await
150+ .context("Error downloading release")?;
151+ if !response.status().is_success() {
152+ Err(anyhow!(
153+ "download failed with status {}",
154+ response.status().to_string()
155+ ))?;
156+ }
157+
158+ match file_type {
159+ DownloadedFileType::GzipTar => {
160+ let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
161+ let archive = Archive::new(decompressed_bytes);
162+ archive.unpack(&version_path).await?;
163+ }
164+ DownloadedFileType::Zip | DownloadedFileType::Vsix => {
165+ let zip_path = version_path.with_extension("zip");
166+
167+ let mut file = File::create(&zip_path).await?;
168+ futures::io::copy(response.body_mut(), &mut file).await?;
169+
170+ // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
171+ util::command::new_smol_command("unzip")
172+ .arg(&zip_path)
173+ .arg("-d")
174+ .arg(&version_path)
175+ .output()
176+ .await?;
177+
178+ util::fs::remove_matching(&adapter_path, |entry| {
179+ entry
180+ .file_name()
181+ .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
182+ })
183+ .await;
184+ }
185+ }
186+
187+ // remove older versions
188+ util::fs::remove_matching(&adapter_path, |entry| {
189+ entry.to_string_lossy() != version_path.to_string_lossy()
190+ })
191+ .await;
192+
193+ Ok(version_path)
194+}
195+
196+pub async fn fetch_latest_adapter_version_from_github(
197+ github_repo: GithubRepo,
198+ delegate: &dyn DapDelegate,
199+) -> Result<AdapterVersion> {
200+ let release = latest_github_release(
201+ &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
202+ false,
203+ false,
204+ delegate.http_client(),
205+ )
206+ .await?;
207+
208+ Ok(AdapterVersion {
209+ tag_name: release.tag_name,
210+ url: release.zipball_url,
211+ })
212+}
213+
214+#[async_trait(?Send)]
215+pub trait DebugAdapter: 'static + Send + Sync {
216+ fn name(&self) -> DebugAdapterName;
217+
218+ async fn get_binary(
219+ &self,
220+ delegate: &dyn DapDelegate,
221+ config: &DebugAdapterConfig,
222+ user_installed_path: Option<PathBuf>,
223+ cx: &mut AsyncApp,
224+ ) -> Result<DebugAdapterBinary> {
225+ if delegate
226+ .updated_adapters()
227+ .lock()
228+ .await
229+ .contains(&self.name())
230+ {
231+ log::info!("Using cached debug adapter binary {}", self.name());
232+
233+ if let Some(binary) = self
234+ .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
235+ .await
236+ .log_err()
237+ {
238+ return Ok(binary);
239+ }
240+
241+ log::info!(
242+ "Cached binary {} is corrupt falling back to install",
243+ self.name()
244+ );
245+ }
246+
247+ log::info!("Getting latest version of debug adapter {}", self.name());
248+ delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
249+ if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
250+ log::info!(
251+ "Installiing latest version of debug adapter {}",
252+ self.name()
253+ );
254+ delegate.update_status(self.name(), DapStatus::Downloading);
255+ self.install_binary(version, delegate).await?;
256+
257+ delegate
258+ .updated_adapters()
259+ .lock_arc()
260+ .await
261+ .insert(self.name());
262+ }
263+
264+ self.get_installed_binary(delegate, &config, user_installed_path, cx)
265+ .await
266+ }
267+
268+ async fn fetch_latest_adapter_version(
269+ &self,
270+ delegate: &dyn DapDelegate,
271+ ) -> Result<AdapterVersion>;
272+
273+ /// Installs the binary for the debug adapter.
274+ /// This method is called when the adapter binary is not found or needs to be updated.
275+ /// It should download and install the necessary files for the debug adapter to function.
276+ async fn install_binary(
277+ &self,
278+ version: AdapterVersion,
279+ delegate: &dyn DapDelegate,
280+ ) -> Result<()>;
281+
282+ async fn get_installed_binary(
283+ &self,
284+ delegate: &dyn DapDelegate,
285+ config: &DebugAdapterConfig,
286+ user_installed_path: Option<PathBuf>,
287+ cx: &mut AsyncApp,
288+ ) -> Result<DebugAdapterBinary>;
289+
290+ /// Should return base configuration to make the debug adapter work
291+ fn request_args(&self, config: &DebugAdapterConfig) -> Value;
292+}
293+
294+#[cfg(any(test, feature = "test-support"))]
295+pub struct FakeAdapter {}
296+
297+#[cfg(any(test, feature = "test-support"))]
298+impl FakeAdapter {
299+ const ADAPTER_NAME: &'static str = "fake-adapter";
300+
301+ pub fn new() -> Self {
302+ Self {}
303+ }
304+}
305+
306+#[cfg(any(test, feature = "test-support"))]
307+#[async_trait(?Send)]
308+impl DebugAdapter for FakeAdapter {
309+ fn name(&self) -> DebugAdapterName {
310+ DebugAdapterName(Self::ADAPTER_NAME.into())
311+ }
312+
313+ async fn get_binary(
314+ &self,
315+ _: &dyn DapDelegate,
316+ _: &DebugAdapterConfig,
317+ _: Option<PathBuf>,
318+ _: &mut AsyncApp,
319+ ) -> Result<DebugAdapterBinary> {
320+ Ok(DebugAdapterBinary {
321+ command: "command".into(),
322+ arguments: None,
323+ connection: None,
324+ envs: None,
325+ cwd: None,
326+ })
327+ }
328+
329+ async fn fetch_latest_adapter_version(
330+ &self,
331+ _delegate: &dyn DapDelegate,
332+ ) -> Result<AdapterVersion> {
333+ unimplemented!("fetch latest adapter version");
334+ }
335+
336+ async fn install_binary(
337+ &self,
338+ _version: AdapterVersion,
339+ _delegate: &dyn DapDelegate,
340+ ) -> Result<()> {
341+ unimplemented!("install binary");
342+ }
343+
344+ async fn get_installed_binary(
345+ &self,
346+ _: &dyn DapDelegate,
347+ _: &DebugAdapterConfig,
348+ _: Option<PathBuf>,
349+ _: &mut AsyncApp,
350+ ) -> Result<DebugAdapterBinary> {
351+ unimplemented!("get installed binary");
352+ }
353+
354+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
355+ use serde_json::json;
356+ use task::DebugRequestType;
357+
358+ json!({
359+ "request": match config.request {
360+ DebugRequestType::Launch => "launch",
361+ DebugRequestType::Attach(_) => "attach",
362+ },
363+ "process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
364+ attach_config.process_id
365+ } else {
366+ None
367+ },
368+ })
369+ }
370+}
crates/dap/src/client.rs+490−0
@@ -0,0 +1,490 @@
1+use crate::{
2+ adapters::{DebugAdapterBinary, DebugAdapterName},
3+ transport::{IoKind, LogKind, TransportDelegate},
4+};
5+use anyhow::{anyhow, Result};
6+use dap_types::{
7+ messages::{Message, Response},
8+ requests::Request,
9+};
10+use futures::{channel::oneshot, select, FutureExt as _};
11+use gpui::{AppContext, AsyncApp, BackgroundExecutor};
12+use smol::channel::{Receiver, Sender};
13+use std::{
14+ hash::Hash,
15+ sync::atomic::{AtomicU64, Ordering},
16+ time::Duration,
17+};
18+
19+#[cfg(any(test, feature = "test-support"))]
20+const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
21+
22+#[cfg(not(any(test, feature = "test-support")))]
23+const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
24+
25+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26+#[repr(transparent)]
27+pub struct SessionId(pub u32);
28+
29+impl SessionId {
30+ pub fn from_proto(client_id: u64) -> Self {
31+ Self(client_id as u32)
32+ }
33+
34+ pub fn to_proto(&self) -> u64 {
35+ self.0 as u64
36+ }
37+}
38+
39+/// Represents a connection to the debug adapter process, either via stdout/stdin or a socket.
40+pub struct DebugAdapterClient {
41+ id: SessionId,
42+ name: DebugAdapterName,
43+ sequence_count: AtomicU64,
44+ binary: DebugAdapterBinary,
45+ executor: BackgroundExecutor,
46+ transport_delegate: TransportDelegate,
47+}
48+
49+pub type DapMessageHandler = Box<dyn FnMut(Message) + 'static + Send + Sync>;
50+
51+impl DebugAdapterClient {
52+ pub async fn start(
53+ id: SessionId,
54+ name: DebugAdapterName,
55+ binary: DebugAdapterBinary,
56+ message_handler: DapMessageHandler,
57+ cx: AsyncApp,
58+ ) -> Result<Self> {
59+ let ((server_rx, server_tx), transport_delegate) =
60+ TransportDelegate::start(&binary, cx.clone()).await?;
61+ let this = Self {
62+ id,
63+ name,
64+ binary,
65+ transport_delegate,
66+ sequence_count: AtomicU64::new(1),
67+ executor: cx.background_executor().clone(),
68+ };
69+ log::info!("Successfully connected to debug adapter");
70+
71+ let client_id = this.id;
72+
73+ // start handling events/reverse requests
74+
75+ cx.background_spawn(Self::handle_receive_messages(
76+ client_id,
77+ server_rx,
78+ server_tx.clone(),
79+ message_handler,
80+ ))
81+ .detach();
82+
83+ Ok(this)
84+ }
85+
86+ pub async fn reconnect(
87+ &self,
88+ session_id: SessionId,
89+ binary: DebugAdapterBinary,
90+ message_handler: DapMessageHandler,
91+ cx: AsyncApp,
92+ ) -> Result<Self> {
93+ let binary = match self.transport_delegate.transport() {
94+ crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
95+ command: binary.command,
96+ arguments: binary.arguments,
97+ envs: binary.envs,
98+ cwd: binary.cwd,
99+ connection: Some(crate::adapters::TcpArguments {
100+ host: tcp_transport.host,
101+ port: tcp_transport.port,
102+ timeout: Some(tcp_transport.timeout),
103+ }),
104+ },
105+ _ => self.binary.clone(),
106+ };
107+
108+ Self::start(session_id, self.name(), binary, message_handler, cx).await
109+ }
110+
111+ async fn handle_receive_messages(
112+ client_id: SessionId,
113+ server_rx: Receiver<Message>,
114+ client_tx: Sender<Message>,
115+ mut message_handler: DapMessageHandler,
116+ ) -> Result<()> {
117+ let result = loop {
118+ let message = match server_rx.recv().await {
119+ Ok(message) => message,
120+ Err(e) => break Err(e.into()),
121+ };
122+
123+ match message {
124+ Message::Event(ev) => {
125+ log::debug!("Client {} received event `{}`", client_id.0, &ev);
126+
127+ message_handler(Message::Event(ev))
128+ }
129+ Message::Request(req) => {
130+ log::debug!(
131+ "Client {} received reverse request `{}`",
132+ client_id.0,
133+ &req.command
134+ );
135+
136+ message_handler(Message::Request(req))
137+ }
138+ Message::Response(response) => {
139+ log::debug!("Received response after request timeout: {:#?}", response);
140+ }
141+ }
142+
143+ smol::future::yield_now().await;
144+ };
145+
146+ drop(client_tx);
147+
148+ log::debug!("Handle receive messages dropped");
149+
150+ result
151+ }
152+
153+ /// Send a request to an adapter and get a response back
154+ /// Note: This function will block until a response is sent back from the adapter
155+ pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
156+ let serialized_arguments = serde_json::to_value(arguments)?;
157+
158+ let (callback_tx, callback_rx) = oneshot::channel::<Result<Response>>();
159+
160+ let sequence_id = self.next_sequence_id();
161+
162+ let request = crate::messages::Request {
163+ seq: sequence_id,
164+ command: R::COMMAND.to_string(),
165+ arguments: Some(serialized_arguments),
166+ };
167+
168+ self.transport_delegate
169+ .add_pending_request(sequence_id, callback_tx)
170+ .await;
171+
172+ log::debug!(
173+ "Client {} send `{}` request with sequence_id: {}",
174+ self.id.0,
175+ R::COMMAND.to_string(),
176+ sequence_id
177+ );
178+
179+ self.send_message(Message::Request(request)).await?;
180+
181+ let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse();
182+ let command = R::COMMAND.to_string();
183+
184+ select! {
185+ response = callback_rx.fuse() => {
186+ log::debug!(
187+ "Client {} received response for: `{}` sequence_id: {}",
188+ self.id.0,
189+ command,
190+ sequence_id
191+ );
192+
193+ let response = response??;
194+ match response.success {
195+ true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
196+ false => Err(anyhow!("Request failed: {}", response.message.unwrap_or_default())),
197+ }
198+ }
199+
200+ _ = timeout => {
201+ self.transport_delegate.cancel_pending_request(&sequence_id).await;
202+ log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}");
203+ anyhow::bail!("DAP request timeout");
204+ }
205+ }
206+ }
207+
208+ pub async fn send_message(&self, message: Message) -> Result<()> {
209+ self.transport_delegate.send_message(message).await
210+ }
211+
212+ pub fn id(&self) -> SessionId {
213+ self.id
214+ }
215+
216+ pub fn name(&self) -> DebugAdapterName {
217+ self.name.clone()
218+ }
219+ pub fn binary(&self) -> &DebugAdapterBinary {
220+ &self.binary
221+ }
222+
223+ /// Get the next sequence id to be used in a request
224+ pub fn next_sequence_id(&self) -> u64 {
225+ self.sequence_count.fetch_add(1, Ordering::Relaxed)
226+ }
227+
228+ pub async fn shutdown(&self) -> Result<()> {
229+ self.transport_delegate.shutdown().await
230+ }
231+
232+ pub fn has_adapter_logs(&self) -> bool {
233+ self.transport_delegate.has_adapter_logs()
234+ }
235+
236+ pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
237+ where
238+ F: 'static + Send + FnMut(IoKind, &str),
239+ {
240+ self.transport_delegate.add_log_handler(f, kind);
241+ }
242+
243+ #[cfg(any(test, feature = "test-support"))]
244+ pub async fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
245+ where
246+ F: 'static
247+ + Send
248+ + FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
249+ {
250+ let transport = self.transport_delegate.transport().as_fake();
251+ transport.on_request::<R, F>(handler).await;
252+ }
253+
254+ #[cfg(any(test, feature = "test-support"))]
255+ pub async fn fake_reverse_request<R: dap_types::requests::Request>(&self, args: R::Arguments) {
256+ self.send_message(Message::Request(dap_types::messages::Request {
257+ seq: self.sequence_count.load(Ordering::Relaxed),
258+ command: R::COMMAND.into(),
259+ arguments: serde_json::to_value(args).ok(),
260+ }))
261+ .await
262+ .unwrap();
263+ }
264+
265+ #[cfg(any(test, feature = "test-support"))]
266+ pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
267+ where
268+ F: 'static + Send + Fn(Response),
269+ {
270+ let transport = self.transport_delegate.transport().as_fake();
271+ transport.on_response::<R, F>(handler).await;
272+ }
273+
274+ #[cfg(any(test, feature = "test-support"))]
275+ pub async fn fake_event(&self, event: dap_types::messages::Events) {
276+ self.send_message(Message::Event(Box::new(event)))
277+ .await
278+ .unwrap();
279+ }
280+}
281+
282+#[cfg(test)]
283+mod tests {
284+ use super::*;
285+ use crate::{client::DebugAdapterClient, debugger_settings::DebuggerSettings};
286+ use dap_types::{
287+ messages::Events,
288+ requests::{Initialize, Request, RunInTerminal},
289+ Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat,
290+ RunInTerminalRequestArguments,
291+ };
292+ use gpui::TestAppContext;
293+ use serde_json::json;
294+ use settings::{Settings, SettingsStore};
295+ use std::sync::{
296+ atomic::{AtomicBool, Ordering},
297+ Arc,
298+ };
299+
300+ pub fn init_test(cx: &mut gpui::TestAppContext) {
301+ if std::env::var("RUST_LOG").is_ok() {
302+ env_logger::try_init().ok();
303+ }
304+
305+ cx.update(|cx| {
306+ let settings = SettingsStore::test(cx);
307+ cx.set_global(settings);
308+ DebuggerSettings::register(cx);
309+ });
310+ }
311+
312+ #[gpui::test]
313+ pub async fn test_initialize_client(cx: &mut TestAppContext) {
314+ init_test(cx);
315+
316+ let client = DebugAdapterClient::start(
317+ crate::client::SessionId(1),
318+ DebugAdapterName("adapter".into()),
319+ DebugAdapterBinary {
320+ command: "command".into(),
321+ arguments: Default::default(),
322+ envs: Default::default(),
323+ connection: None,
324+ cwd: None,
325+ },
326+ Box::new(|_| panic!("Did not expect to hit this code path")),
327+ cx.to_async(),
328+ )
329+ .await
330+ .unwrap();
331+
332+ client
333+ .on_request::<Initialize, _>(move |_, _| {
334+ Ok(dap_types::Capabilities {
335+ supports_configuration_done_request: Some(true),
336+ ..Default::default()
337+ })
338+ })
339+ .await;
340+
341+ cx.run_until_parked();
342+
343+ let response = client
344+ .request::<Initialize>(InitializeRequestArguments {
345+ client_id: Some("zed".to_owned()),
346+ client_name: Some("Zed".to_owned()),
347+ adapter_id: "fake-adapter".to_owned(),
348+ locale: Some("en-US".to_owned()),
349+ path_format: Some(InitializeRequestArgumentsPathFormat::Path),
350+ supports_variable_type: Some(true),
351+ supports_variable_paging: Some(false),
352+ supports_run_in_terminal_request: Some(true),
353+ supports_memory_references: Some(true),
354+ supports_progress_reporting: Some(false),
355+ supports_invalidated_event: Some(false),
356+ lines_start_at1: Some(true),
357+ columns_start_at1: Some(true),
358+ supports_memory_event: Some(false),
359+ supports_args_can_be_interpreted_by_shell: Some(false),
360+ supports_start_debugging_request: Some(true),
361+ supports_ansistyling: Some(false),
362+ })
363+ .await
364+ .unwrap();
365+
366+ cx.run_until_parked();
367+
368+ assert_eq!(
369+ dap_types::Capabilities {
370+ supports_configuration_done_request: Some(true),
371+ ..Default::default()
372+ },
373+ response
374+ );
375+
376+ client.shutdown().await.unwrap();
377+ }
378+
379+ #[gpui::test]
380+ pub async fn test_calls_event_handler(cx: &mut TestAppContext) {
381+ init_test(cx);
382+
383+ let called_event_handler = Arc::new(AtomicBool::new(false));
384+
385+ let client = DebugAdapterClient::start(
386+ crate::client::SessionId(1),
387+ DebugAdapterName("adapter".into()),
388+ DebugAdapterBinary {
389+ command: "command".into(),
390+ arguments: Default::default(),
391+ envs: Default::default(),
392+ connection: None,
393+ cwd: None,
394+ },
395+ Box::new({
396+ let called_event_handler = called_event_handler.clone();
397+ move |event| {
398+ called_event_handler.store(true, Ordering::SeqCst);
399+
400+ assert_eq!(
401+ Message::Event(Box::new(Events::Initialized(
402+ Some(Capabilities::default())
403+ ))),
404+ event
405+ );
406+ }
407+ }),
408+ cx.to_async(),
409+ )
410+ .await
411+ .unwrap();
412+
413+ cx.run_until_parked();
414+
415+ client
416+ .fake_event(Events::Initialized(Some(Capabilities::default())))
417+ .await;
418+
419+ cx.run_until_parked();
420+
421+ assert!(
422+ called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
423+ "Event handler was not called"
424+ );
425+
426+ client.shutdown().await.unwrap();
427+ }
428+
429+ #[gpui::test]
430+ pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) {
431+ init_test(cx);
432+
433+ let called_event_handler = Arc::new(AtomicBool::new(false));
434+
435+ let client = DebugAdapterClient::start(
436+ crate::client::SessionId(1),
437+ DebugAdapterName(Arc::from("test-adapter")),
438+ DebugAdapterBinary {
439+ command: "command".into(),
440+ arguments: Default::default(),
441+ envs: Default::default(),
442+ connection: None,
443+ cwd: None,
444+ },
445+ Box::new({
446+ let called_event_handler = called_event_handler.clone();
447+ move |event| {
448+ called_event_handler.store(true, Ordering::SeqCst);
449+
450+ assert_eq!(
451+ Message::Request(dap_types::messages::Request {
452+ seq: 1,
453+ command: RunInTerminal::COMMAND.into(),
454+ arguments: Some(json!({
455+ "cwd": "/project/path/src",
456+ "args": ["node", "test.js"],
457+ }))
458+ }),
459+ event
460+ );
461+ }
462+ }),
463+ cx.to_async(),
464+ )
465+ .await
466+ .unwrap();
467+
468+ cx.run_until_parked();
469+
470+ client
471+ .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
472+ kind: None,
473+ title: None,
474+ cwd: "/project/path/src".into(),
475+ args: vec!["node".into(), "test.js".into()],
476+ env: None,
477+ args_can_be_interpreted_by_shell: None,
478+ })
479+ .await;
480+
481+ cx.run_until_parked();
482+
483+ assert!(
484+ called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
485+ "Event handler was not called"
486+ );
487+
488+ client.shutdown().await.unwrap();
489+ }
490+}
crates/dap/src/debugger_settings.rs+59−0
@@ -0,0 +1,59 @@
1+use dap_types::SteppingGranularity;
2+use gpui::{App, Global};
3+use schemars::JsonSchema;
4+use serde::{Deserialize, Serialize};
5+use settings::{Settings, SettingsSources};
6+
7+#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
8+#[serde(default)]
9+pub struct DebuggerSettings {
10+ /// Determines the stepping granularity.
11+ ///
12+ /// Default: line
13+ pub stepping_granularity: SteppingGranularity,
14+ /// Whether the breakpoints should be reused across Zed sessions.
15+ ///
16+ /// Default: true
17+ pub save_breakpoints: bool,
18+ /// Whether to show the debug button in the status bar.
19+ ///
20+ /// Default: true
21+ pub button: bool,
22+ /// Time in milliseconds until timeout error when connecting to a TCP debug adapter
23+ ///
24+ /// Default: 2000ms
25+ pub timeout: u64,
26+ /// Whether to log messages between active debug adapters and Zed
27+ ///
28+ /// Default: true
29+ pub log_dap_communications: bool,
30+ /// Whether to format dap messages in when adding them to debug adapter logger
31+ ///
32+ /// Default: true
33+ pub format_dap_log_messages: bool,
34+}
35+
36+impl Default for DebuggerSettings {
37+ fn default() -> Self {
38+ Self {
39+ button: true,
40+ save_breakpoints: true,
41+ stepping_granularity: SteppingGranularity::Line,
42+ timeout: 2000,
43+ log_dap_communications: true,
44+ format_dap_log_messages: true,
45+ }
46+ }
47+}
48+
49+impl Settings for DebuggerSettings {
50+ const KEY: Option<&'static str> = Some("debugger");
51+
52+ type FileContent = Self;
53+
54+ fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
55+ sources.json_merge()
56+ }
57+}
58+
59+impl Global for DebuggerSettings {}
crates/dap/src/lib.rs+38−0
@@ -0,0 +1,38 @@
1+pub mod adapters;
2+pub mod client;
3+pub mod debugger_settings;
4+pub mod proto_conversions;
5+pub mod transport;
6+
7+pub use dap_types::*;
8+pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
9+
10+pub type ScopeId = u64;
11+pub type VariableReference = u64;
12+pub type StackFrameId = u64;
13+
14+#[cfg(any(test, feature = "test-support"))]
15+pub use adapters::FakeAdapter;
16+
17+#[cfg(any(test, feature = "test-support"))]
18+pub fn test_config(
19+ request: DebugRequestType,
20+ fail: Option<bool>,
21+ caps: Option<Capabilities>,
22+) -> DebugAdapterConfig {
23+ DebugAdapterConfig {
24+ label: "test config".into(),
25+ kind: DebugAdapterKind::Fake((
26+ fail.unwrap_or_default(),
27+ caps.unwrap_or(Capabilities {
28+ supports_step_back: Some(false),
29+ ..Default::default()
30+ }),
31+ )),
32+ request,
33+ program: None,
34+ supports_attach: false,
35+ cwd: None,
36+ initialize_args: None,
37+ }
38+}
crates/dap/src/proto_conversions.rs+591−0
@@ -0,0 +1,591 @@
1+use anyhow::{anyhow, Result};
2+use client::proto::{
3+ self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
4+ DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
5+};
6+use dap_types::{OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source};
7+
8+pub trait ProtoConversion {
9+ type ProtoType;
10+ type Output;
11+
12+ fn to_proto(&self) -> Self::ProtoType;
13+ fn from_proto(payload: Self::ProtoType) -> Self::Output;
14+}
15+
16+impl<T> ProtoConversion for Vec<T>
17+where
18+ T: ProtoConversion<Output = T>,
19+{
20+ type ProtoType = Vec<T::ProtoType>;
21+ type Output = Self;
22+
23+ fn to_proto(&self) -> Self::ProtoType {
24+ self.iter().map(|item| item.to_proto()).collect()
25+ }
26+
27+ fn from_proto(payload: Self::ProtoType) -> Self {
28+ payload
29+ .into_iter()
30+ .map(|item| T::from_proto(item))
31+ .collect()
32+ }
33+}
34+
35+impl ProtoConversion for dap_types::Scope {
36+ type ProtoType = DapScope;
37+ type Output = Self;
38+
39+ fn to_proto(&self) -> Self::ProtoType {
40+ Self::ProtoType {
41+ name: self.name.clone(),
42+ presentation_hint: self
43+ .presentation_hint
44+ .as_ref()
45+ .map(|hint| hint.to_proto().into()),
46+ variables_reference: self.variables_reference,
47+ named_variables: self.named_variables,
48+ indexed_variables: self.indexed_variables,
49+ expensive: self.expensive,
50+ source: self.source.as_ref().map(Source::to_proto),
51+ line: self.line,
52+ end_line: self.end_line,
53+ column: self.column,
54+ end_column: self.end_column,
55+ }
56+ }
57+
58+ fn from_proto(payload: Self::ProtoType) -> Self {
59+ let presentation_hint = payload
60+ .presentation_hint
61+ .and_then(DapScopePresentationHint::from_i32);
62+ Self {
63+ name: payload.name,
64+ presentation_hint: presentation_hint.map(ScopePresentationHint::from_proto),
65+ variables_reference: payload.variables_reference,
66+ named_variables: payload.named_variables,
67+ indexed_variables: payload.indexed_variables,
68+ expensive: payload.expensive,
69+ source: payload.source.map(dap_types::Source::from_proto),
70+ line: payload.line,
71+ end_line: payload.end_line,
72+ column: payload.column,
73+ end_column: payload.end_column,
74+ }
75+ }
76+}
77+
78+impl ProtoConversion for dap_types::Variable {
79+ type ProtoType = DapVariable;
80+ type Output = Self;
81+
82+ fn to_proto(&self) -> Self::ProtoType {
83+ Self::ProtoType {
84+ name: self.name.clone(),
85+ value: self.value.clone(),
86+ r#type: self.type_.clone(),
87+ evaluate_name: self.evaluate_name.clone(),
88+ variables_reference: self.variables_reference,
89+ named_variables: self.named_variables,
90+ indexed_variables: self.indexed_variables,
91+ memory_reference: self.memory_reference.clone(),
92+ }
93+ }
94+
95+ fn from_proto(payload: Self::ProtoType) -> Self {
96+ Self {
97+ name: payload.name,
98+ value: payload.value,
99+ type_: payload.r#type,
100+ evaluate_name: payload.evaluate_name,
101+ presentation_hint: None, // TODO Debugger Collab Add this
102+ variables_reference: payload.variables_reference,
103+ named_variables: payload.named_variables,
104+ indexed_variables: payload.indexed_variables,
105+ memory_reference: payload.memory_reference,
106+ declaration_location_reference: None, // TODO
107+ value_location_reference: None, // TODO
108+ }
109+ }
110+}
111+
112+impl ProtoConversion for dap_types::ScopePresentationHint {
113+ type ProtoType = DapScopePresentationHint;
114+ type Output = Self;
115+
116+ fn to_proto(&self) -> Self::ProtoType {
117+ match self {
118+ dap_types::ScopePresentationHint::Locals => DapScopePresentationHint::Locals,
119+ dap_types::ScopePresentationHint::Arguments => DapScopePresentationHint::Arguments,
120+ dap_types::ScopePresentationHint::Registers => DapScopePresentationHint::Registers,
121+ dap_types::ScopePresentationHint::ReturnValue => DapScopePresentationHint::ReturnValue,
122+ dap_types::ScopePresentationHint::Unknown => DapScopePresentationHint::ScopeUnknown,
123+ &_ => unreachable!(),
124+ }
125+ }
126+
127+ fn from_proto(payload: Self::ProtoType) -> Self {
128+ match payload {
129+ DapScopePresentationHint::Locals => dap_types::ScopePresentationHint::Locals,
130+ DapScopePresentationHint::Arguments => dap_types::ScopePresentationHint::Arguments,
131+ DapScopePresentationHint::Registers => dap_types::ScopePresentationHint::Registers,
132+ DapScopePresentationHint::ReturnValue => dap_types::ScopePresentationHint::ReturnValue,
133+ DapScopePresentationHint::ScopeUnknown => dap_types::ScopePresentationHint::Unknown,
134+ }
135+ }
136+}
137+
138+impl ProtoConversion for dap_types::SourcePresentationHint {
139+ type ProtoType = DapSourcePresentationHint;
140+ type Output = Self;
141+
142+ fn to_proto(&self) -> Self::ProtoType {
143+ match self {
144+ dap_types::SourcePresentationHint::Normal => DapSourcePresentationHint::SourceNormal,
145+ dap_types::SourcePresentationHint::Emphasize => DapSourcePresentationHint::Emphasize,
146+ dap_types::SourcePresentationHint::Deemphasize => {
147+ DapSourcePresentationHint::Deemphasize
148+ }
149+ dap_types::SourcePresentationHint::Unknown => DapSourcePresentationHint::SourceUnknown,
150+ }
151+ }
152+
153+ fn from_proto(payload: Self::ProtoType) -> Self {
154+ match payload {
155+ DapSourcePresentationHint::SourceNormal => dap_types::SourcePresentationHint::Normal,
156+ DapSourcePresentationHint::Emphasize => dap_types::SourcePresentationHint::Emphasize,
157+ DapSourcePresentationHint::Deemphasize => {
158+ dap_types::SourcePresentationHint::Deemphasize
159+ }
160+ DapSourcePresentationHint::SourceUnknown => dap_types::SourcePresentationHint::Unknown,
161+ }
162+ }
163+}
164+
165+impl ProtoConversion for dap_types::Checksum {
166+ type ProtoType = DapChecksum;
167+ type Output = Self;
168+
169+ fn to_proto(&self) -> Self::ProtoType {
170+ DapChecksum {
171+ algorithm: self.algorithm.to_proto().into(),
172+ checksum: self.checksum.clone(),
173+ }
174+ }
175+
176+ fn from_proto(payload: Self::ProtoType) -> Self {
177+ Self {
178+ algorithm: dap_types::ChecksumAlgorithm::from_proto(payload.algorithm()),
179+ checksum: payload.checksum,
180+ }
181+ }
182+}
183+
184+impl ProtoConversion for dap_types::ChecksumAlgorithm {
185+ type ProtoType = DapChecksumAlgorithm;
186+ type Output = Self;
187+
188+ fn to_proto(&self) -> Self::ProtoType {
189+ match self {
190+ dap_types::ChecksumAlgorithm::Md5 => DapChecksumAlgorithm::Md5,
191+ dap_types::ChecksumAlgorithm::Sha1 => DapChecksumAlgorithm::Sha1,
192+ dap_types::ChecksumAlgorithm::Sha256 => DapChecksumAlgorithm::Sha256,
193+ dap_types::ChecksumAlgorithm::Timestamp => DapChecksumAlgorithm::Timestamp,
194+ }
195+ }
196+
197+ fn from_proto(payload: Self::ProtoType) -> Self {
198+ match payload {
199+ DapChecksumAlgorithm::Md5 => dap_types::ChecksumAlgorithm::Md5,
200+ DapChecksumAlgorithm::Sha1 => dap_types::ChecksumAlgorithm::Sha1,
201+ DapChecksumAlgorithm::Sha256 => dap_types::ChecksumAlgorithm::Sha256,
202+ DapChecksumAlgorithm::Timestamp => dap_types::ChecksumAlgorithm::Timestamp,
203+ DapChecksumAlgorithm::ChecksumAlgorithmUnspecified => unreachable!(),
204+ }
205+ }
206+}
207+
208+impl ProtoConversion for dap_types::Source {
209+ type ProtoType = DapSource;
210+ type Output = Self;
211+
212+ fn to_proto(&self) -> Self::ProtoType {
213+ Self::ProtoType {
214+ name: self.name.clone(),
215+ path: self.path.clone(),
216+ source_reference: self.source_reference,
217+ presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()),
218+ origin: self.origin.clone(),
219+ sources: self
220+ .sources
221+ .clone()
222+ .map(|src| src.to_proto())
223+ .unwrap_or_default(),
224+ adapter_data: Default::default(), // TODO Debugger Collab
225+ checksums: self
226+ .checksums
227+ .clone()
228+ .map(|c| c.to_proto())
229+ .unwrap_or_default(),
230+ }
231+ }
232+
233+ fn from_proto(payload: Self::ProtoType) -> Self {
234+ Self {
235+ name: payload.name.clone(),
236+ path: payload.path.clone(),
237+ source_reference: payload.source_reference,
238+ presentation_hint: payload
239+ .presentation_hint
240+ .and_then(DapSourcePresentationHint::from_i32)
241+ .map(dap_types::SourcePresentationHint::from_proto),
242+ origin: payload.origin.clone(),
243+ sources: Some(Vec::<dap_types::Source>::from_proto(payload.sources)),
244+ checksums: Some(Vec::<dap_types::Checksum>::from_proto(payload.checksums)),
245+ adapter_data: None, // TODO Debugger Collab
246+ }
247+ }
248+}
249+
250+impl ProtoConversion for dap_types::StackFrame {
251+ type ProtoType = DapStackFrame;
252+ type Output = Self;
253+
254+ fn to_proto(&self) -> Self::ProtoType {
255+ Self::ProtoType {
256+ id: self.id,
257+ name: self.name.clone(),
258+ source: self.source.as_ref().map(|src| src.to_proto()),
259+ line: self.line,
260+ column: self.column,
261+ end_line: self.end_line,
262+ end_column: self.end_column,
263+ can_restart: self.can_restart,
264+ instruction_pointer_reference: self.instruction_pointer_reference.clone(),
265+ module_id: None, // TODO Debugger Collab
266+ presentation_hint: None, // TODO Debugger Collab
267+ }
268+ }
269+
270+ fn from_proto(payload: Self::ProtoType) -> Self {
271+ Self {
272+ id: payload.id,
273+ name: payload.name,
274+ source: payload.source.map(dap_types::Source::from_proto),
275+ line: payload.line,
276+ column: payload.column,
277+ end_line: payload.end_line,
278+ end_column: payload.end_column,
279+ can_restart: payload.can_restart,
280+ instruction_pointer_reference: payload.instruction_pointer_reference,
281+ module_id: None, // TODO Debugger Collab
282+ presentation_hint: None, // TODO Debugger Collab
283+ }
284+ }
285+}
286+
287+impl ProtoConversion for dap_types::Module {
288+ type ProtoType = DapModule;
289+ type Output = Result<Self>;
290+
291+ fn to_proto(&self) -> Self::ProtoType {
292+ let id = match &self.id {
293+ dap_types::ModuleId::Number(num) => proto::dap_module_id::Id::Number(*num),
294+ dap_types::ModuleId::String(string) => proto::dap_module_id::Id::String(string.clone()),
295+ };
296+
297+ DapModule {
298+ id: Some(proto::DapModuleId { id: Some(id) }),
299+ name: self.name.clone(),
300+ path: self.path.clone(),
301+ is_optimized: self.is_optimized,
302+ is_user_code: self.is_user_code,
303+ version: self.version.clone(),
304+ symbol_status: self.symbol_status.clone(),
305+ symbol_file_path: self.symbol_file_path.clone(),
306+ date_time_stamp: self.date_time_stamp.clone(),
307+ address_range: self.address_range.clone(),
308+ }
309+ }
310+
311+ fn from_proto(payload: Self::ProtoType) -> Result<Self> {
312+ let id = match payload
313+ .id
314+ .ok_or(anyhow!("All DapModule proto messages must have an id"))?
315+ .id
316+ .ok_or(anyhow!("All DapModuleID proto messages must have an id"))?
317+ {
318+ proto::dap_module_id::Id::String(string) => dap_types::ModuleId::String(string),
319+ proto::dap_module_id::Id::Number(num) => dap_types::ModuleId::Number(num),
320+ };
321+
322+ Ok(Self {
323+ id,
324+ name: payload.name,
325+ path: payload.path,
326+ is_optimized: payload.is_optimized,
327+ is_user_code: payload.is_user_code,
328+ version: payload.version,
329+ symbol_status: payload.symbol_status,
330+ symbol_file_path: payload.symbol_file_path,
331+ date_time_stamp: payload.date_time_stamp,
332+ address_range: payload.address_range,
333+ })
334+ }
335+}
336+
337+impl ProtoConversion for dap_types::SteppingGranularity {
338+ type ProtoType = proto::SteppingGranularity;
339+ type Output = Self;
340+
341+ fn to_proto(&self) -> Self::ProtoType {
342+ match self {
343+ dap_types::SteppingGranularity::Statement => proto::SteppingGranularity::Statement,
344+ dap_types::SteppingGranularity::Line => proto::SteppingGranularity::Line,
345+ dap_types::SteppingGranularity::Instruction => proto::SteppingGranularity::Instruction,
346+ }
347+ }
348+
349+ fn from_proto(payload: Self::ProtoType) -> Self {
350+ match payload {
351+ proto::SteppingGranularity::Line => dap_types::SteppingGranularity::Line,
352+ proto::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction,
353+ proto::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement,
354+ }
355+ }
356+}
357+
358+impl ProtoConversion for dap_types::OutputEventCategory {
359+ type ProtoType = proto::DapOutputCategory;
360+ type Output = Self;
361+
362+ fn to_proto(&self) -> Self::ProtoType {
363+ match self {
364+ Self::Console => proto::DapOutputCategory::ConsoleOutput,
365+ Self::Important => proto::DapOutputCategory::Important,
366+ Self::Stdout => proto::DapOutputCategory::Stdout,
367+ Self::Stderr => proto::DapOutputCategory::Stderr,
368+ _ => proto::DapOutputCategory::Unknown,
369+ }
370+ }
371+
372+ fn from_proto(payload: Self::ProtoType) -> Self {
373+ match payload {
374+ proto::DapOutputCategory::ConsoleOutput => Self::Console,
375+ proto::DapOutputCategory::Important => Self::Important,
376+ proto::DapOutputCategory::Stdout => Self::Stdout,
377+ proto::DapOutputCategory::Stderr => Self::Stderr,
378+ proto::DapOutputCategory::Unknown => Self::Unknown,
379+ }
380+ }
381+}
382+
383+impl ProtoConversion for dap_types::OutputEvent {
384+ type ProtoType = proto::DapOutputEvent;
385+ type Output = Self;
386+
387+ fn to_proto(&self) -> Self::ProtoType {
388+ proto::DapOutputEvent {
389+ category: self
390+ .category
391+ .as_ref()
392+ .map(|category| category.to_proto().into()),
393+ output: self.output.clone(),
394+ variables_reference: self.variables_reference,
395+ source: self.source.as_ref().map(|source| source.to_proto()),
396+ line: self.line.map(|line| line as u32),
397+ column: self.column.map(|column| column as u32),
398+ group: self.group.map(|group| group.to_proto().into()),
399+ }
400+ }
401+
402+ fn from_proto(payload: Self::ProtoType) -> Self {
403+ dap_types::OutputEvent {
404+ category: payload
405+ .category
406+ .and_then(proto::DapOutputCategory::from_i32)
407+ .map(OutputEventCategory::from_proto),
408+ output: payload.output.clone(),
409+ variables_reference: payload.variables_reference,
410+ source: payload.source.map(Source::from_proto),
411+ line: payload.line.map(|line| line as u64),
412+ column: payload.column.map(|column| column as u64),
413+ group: payload
414+ .group
415+ .and_then(proto::DapOutputEventGroup::from_i32)
416+ .map(OutputEventGroup::from_proto),
417+ data: None,
418+ location_reference: None,
419+ }
420+ }
421+}
422+
423+impl ProtoConversion for dap_types::OutputEventGroup {
424+ type ProtoType = proto::DapOutputEventGroup;
425+ type Output = Self;
426+
427+ fn to_proto(&self) -> Self::ProtoType {
428+ match self {
429+ dap_types::OutputEventGroup::Start => proto::DapOutputEventGroup::Start,
430+ dap_types::OutputEventGroup::StartCollapsed => {
431+ proto::DapOutputEventGroup::StartCollapsed
432+ }
433+ dap_types::OutputEventGroup::End => proto::DapOutputEventGroup::End,
434+ }
435+ }
436+
437+ fn from_proto(payload: Self::ProtoType) -> Self {
438+ match payload {
439+ proto::DapOutputEventGroup::Start => Self::Start,
440+ proto::DapOutputEventGroup::StartCollapsed => Self::StartCollapsed,
441+ proto::DapOutputEventGroup::End => Self::End,
442+ }
443+ }
444+}
445+
446+impl ProtoConversion for dap_types::CompletionItem {
447+ type ProtoType = proto::DapCompletionItem;
448+ type Output = Self;
449+
450+ fn to_proto(&self) -> Self::ProtoType {
451+ proto::DapCompletionItem {
452+ label: self.label.clone(),
453+ text: self.text.clone(),
454+ detail: self.detail.clone(),
455+ typ: self
456+ .type_
457+ .as_ref()
458+ .map(ProtoConversion::to_proto)
459+ .map(|typ| typ.into()),
460+ start: self.start,
461+ length: self.length,
462+ selection_start: self.selection_start,
463+ selection_length: self.selection_length,
464+ sort_text: self.sort_text.clone(),
465+ }
466+ }
467+
468+ fn from_proto(payload: Self::ProtoType) -> Self {
469+ let typ = payload.typ(); // todo(debugger): This might be a potential issue/bug because it defaults to a type when it's None
470+
471+ Self {
472+ label: payload.label,
473+ detail: payload.detail,
474+ sort_text: payload.sort_text,
475+ text: payload.text.clone(),
476+ type_: Some(dap_types::CompletionItemType::from_proto(typ)),
477+ start: payload.start,
478+ length: payload.length,
479+ selection_start: payload.selection_start,
480+ selection_length: payload.selection_length,
481+ }
482+ }
483+}
484+
485+impl ProtoConversion for dap_types::EvaluateArgumentsContext {
486+ type ProtoType = DapEvaluateContext;
487+ type Output = Self;
488+
489+ fn to_proto(&self) -> Self::ProtoType {
490+ match self {
491+ dap_types::EvaluateArgumentsContext::Variables => {
492+ proto::DapEvaluateContext::EvaluateVariables
493+ }
494+ dap_types::EvaluateArgumentsContext::Watch => proto::DapEvaluateContext::Watch,
495+ dap_types::EvaluateArgumentsContext::Hover => proto::DapEvaluateContext::Hover,
496+ dap_types::EvaluateArgumentsContext::Repl => proto::DapEvaluateContext::Repl,
497+ dap_types::EvaluateArgumentsContext::Clipboard => proto::DapEvaluateContext::Clipboard,
498+ dap_types::EvaluateArgumentsContext::Unknown => {
499+ proto::DapEvaluateContext::EvaluateUnknown
500+ }
501+ _ => proto::DapEvaluateContext::EvaluateUnknown,
502+ }
503+ }
504+
505+ fn from_proto(payload: Self::ProtoType) -> Self {
506+ match payload {
507+ proto::DapEvaluateContext::EvaluateVariables => {
508+ dap_types::EvaluateArgumentsContext::Variables
509+ }
510+ proto::DapEvaluateContext::Watch => dap_types::EvaluateArgumentsContext::Watch,
511+ proto::DapEvaluateContext::Hover => dap_types::EvaluateArgumentsContext::Hover,
512+ proto::DapEvaluateContext::Repl => dap_types::EvaluateArgumentsContext::Repl,
513+ proto::DapEvaluateContext::Clipboard => dap_types::EvaluateArgumentsContext::Clipboard,
514+ proto::DapEvaluateContext::EvaluateUnknown => {
515+ dap_types::EvaluateArgumentsContext::Unknown
516+ }
517+ }
518+ }
519+}
520+
521+impl ProtoConversion for dap_types::CompletionItemType {
522+ type ProtoType = proto::DapCompletionItemType;
523+ type Output = Self;
524+
525+ fn to_proto(&self) -> Self::ProtoType {
526+ match self {
527+ dap_types::CompletionItemType::Class => proto::DapCompletionItemType::Class,
528+ dap_types::CompletionItemType::Color => proto::DapCompletionItemType::Color,
529+ dap_types::CompletionItemType::Constructor => proto::DapCompletionItemType::Constructor,
530+ dap_types::CompletionItemType::Customcolor => proto::DapCompletionItemType::Customcolor,
531+ dap_types::CompletionItemType::Enum => proto::DapCompletionItemType::Enum,
532+ dap_types::CompletionItemType::Field => proto::DapCompletionItemType::Field,
533+ dap_types::CompletionItemType::File => proto::DapCompletionItemType::CompletionItemFile,
534+ dap_types::CompletionItemType::Function => proto::DapCompletionItemType::Function,
535+ dap_types::CompletionItemType::Interface => proto::DapCompletionItemType::Interface,
536+ dap_types::CompletionItemType::Keyword => proto::DapCompletionItemType::Keyword,
537+ dap_types::CompletionItemType::Method => proto::DapCompletionItemType::Method,
538+ dap_types::CompletionItemType::Module => proto::DapCompletionItemType::Module,
539+ dap_types::CompletionItemType::Property => proto::DapCompletionItemType::Property,
540+ dap_types::CompletionItemType::Reference => proto::DapCompletionItemType::Reference,
541+ dap_types::CompletionItemType::Snippet => proto::DapCompletionItemType::Snippet,
542+ dap_types::CompletionItemType::Text => proto::DapCompletionItemType::Text,
543+ dap_types::CompletionItemType::Unit => proto::DapCompletionItemType::Unit,
544+ dap_types::CompletionItemType::Value => proto::DapCompletionItemType::Value,
545+ dap_types::CompletionItemType::Variable => proto::DapCompletionItemType::Variable,
546+ }
547+ }
548+
549+ fn from_proto(payload: Self::ProtoType) -> Self {
550+ match payload {
551+ proto::DapCompletionItemType::Class => dap_types::CompletionItemType::Class,
552+ proto::DapCompletionItemType::Color => dap_types::CompletionItemType::Color,
553+ proto::DapCompletionItemType::CompletionItemFile => dap_types::CompletionItemType::File,
554+ proto::DapCompletionItemType::Constructor => dap_types::CompletionItemType::Constructor,
555+ proto::DapCompletionItemType::Customcolor => dap_types::CompletionItemType::Customcolor,
556+ proto::DapCompletionItemType::Enum => dap_types::CompletionItemType::Enum,
557+ proto::DapCompletionItemType::Field => dap_types::CompletionItemType::Field,
558+ proto::DapCompletionItemType::Function => dap_types::CompletionItemType::Function,
559+ proto::DapCompletionItemType::Interface => dap_types::CompletionItemType::Interface,
560+ proto::DapCompletionItemType::Keyword => dap_types::CompletionItemType::Keyword,
561+ proto::DapCompletionItemType::Method => dap_types::CompletionItemType::Method,
562+ proto::DapCompletionItemType::Module => dap_types::CompletionItemType::Module,
563+ proto::DapCompletionItemType::Property => dap_types::CompletionItemType::Property,
564+ proto::DapCompletionItemType::Reference => dap_types::CompletionItemType::Reference,
565+ proto::DapCompletionItemType::Snippet => dap_types::CompletionItemType::Snippet,
566+ proto::DapCompletionItemType::Text => dap_types::CompletionItemType::Text,
567+ proto::DapCompletionItemType::Unit => dap_types::CompletionItemType::Unit,
568+ proto::DapCompletionItemType::Value => dap_types::CompletionItemType::Value,
569+ proto::DapCompletionItemType::Variable => dap_types::CompletionItemType::Variable,
570+ }
571+ }
572+}
573+
574+impl ProtoConversion for dap_types::Thread {
575+ type ProtoType = proto::DapThread;
576+ type Output = Self;
577+
578+ fn to_proto(&self) -> Self::ProtoType {
579+ proto::DapThread {
580+ id: self.id,
581+ name: self.name.clone(),
582+ }
583+ }
584+
585+ fn from_proto(payload: Self::ProtoType) -> Self {
586+ Self {
587+ id: payload.id,
588+ name: payload.name,
589+ }
590+ }
591+}
crates/dap/src/transport.rs+891−0
@@ -0,0 +1,891 @@
1+use anyhow::{anyhow, bail, Context, Result};
2+use dap_types::{
3+ messages::{Message, Response},
4+ ErrorResponse,
5+};
6+use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _};
7+use gpui::AsyncApp;
8+use settings::Settings as _;
9+use smallvec::SmallVec;
10+use smol::{
11+ channel::{unbounded, Receiver, Sender},
12+ io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
13+ lock::Mutex,
14+ net::{TcpListener, TcpStream},
15+ process::Child,
16+};
17+use std::{
18+ collections::HashMap,
19+ net::{Ipv4Addr, SocketAddrV4},
20+ process::Stdio,
21+ sync::Arc,
22+ time::Duration,
23+};
24+use task::TCPHost;
25+use util::ResultExt as _;
26+
27+use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
28+
29+pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
30+
31+#[derive(PartialEq, Eq, Clone, Copy)]
32+pub enum LogKind {
33+ Adapter,
34+ Rpc,
35+}
36+
37+pub enum IoKind {
38+ StdIn,
39+ StdOut,
40+ StdErr,
41+}
42+
43+pub struct TransportPipe {
44+ input: Box<dyn AsyncWrite + Unpin + Send + 'static>,
45+ output: Box<dyn AsyncRead + Unpin + Send + 'static>,
46+ stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
47+ stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
48+}
49+
50+impl TransportPipe {
51+ pub fn new(
52+ input: Box<dyn AsyncWrite + Unpin + Send + 'static>,
53+ output: Box<dyn AsyncRead + Unpin + Send + 'static>,
54+ stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
55+ stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
56+ ) -> Self {
57+ TransportPipe {
58+ input,
59+ output,
60+ stdout,
61+ stderr,
62+ }
63+ }
64+}
65+
66+type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
67+type LogHandlers = Arc<parking_lot::Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
68+
69+pub enum Transport {
70+ Stdio(StdioTransport),
71+ Tcp(TcpTransport),
72+ #[cfg(any(test, feature = "test-support"))]
73+ Fake(FakeTransport),
74+}
75+
76+impl Transport {
77+ #[cfg(any(test, feature = "test-support"))]
78+ async fn start(_: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
79+ #[cfg(any(test, feature = "test-support"))]
80+ return FakeTransport::start(cx)
81+ .await
82+ .map(|(transports, fake)| (transports, Self::Fake(fake)));
83+ }
84+
85+ #[cfg(not(any(test, feature = "test-support")))]
86+ async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
87+ if binary.connection.is_some() {
88+ TcpTransport::start(binary, cx)
89+ .await
90+ .map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
91+ } else {
92+ StdioTransport::start(binary, cx)
93+ .await
94+ .map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
95+ }
96+ }
97+
98+ fn has_adapter_logs(&self) -> bool {
99+ match self {
100+ Transport::Stdio(stdio_transport) => stdio_transport.has_adapter_logs(),
101+ Transport::Tcp(tcp_transport) => tcp_transport.has_adapter_logs(),
102+ #[cfg(any(test, feature = "test-support"))]
103+ Transport::Fake(fake_transport) => fake_transport.has_adapter_logs(),
104+ }
105+ }
106+
107+ async fn kill(&self) -> Result<()> {
108+ match self {
109+ Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
110+ Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
111+ #[cfg(any(test, feature = "test-support"))]
112+ Transport::Fake(fake_transport) => fake_transport.kill().await,
113+ }
114+ }
115+
116+ #[cfg(any(test, feature = "test-support"))]
117+ pub(crate) fn as_fake(&self) -> &FakeTransport {
118+ match self {
119+ Transport::Fake(fake_transport) => fake_transport,
120+ _ => panic!("Not a fake transport layer"),
121+ }
122+ }
123+}
124+
125+pub(crate) struct TransportDelegate {
126+ log_handlers: LogHandlers,
127+ current_requests: Requests,
128+ pending_requests: Requests,
129+ transport: Transport,
130+ server_tx: Arc<Mutex<Option<Sender<Message>>>>,
131+}
132+
133+impl TransportDelegate {
134+ pub(crate) async fn start(
135+ binary: &DebugAdapterBinary,
136+ cx: AsyncApp,
137+ ) -> Result<((Receiver<Message>, Sender<Message>), Self)> {
138+ let (transport_pipes, transport) = Transport::start(binary, cx.clone()).await?;
139+ let mut this = Self {
140+ transport,
141+ server_tx: Default::default(),
142+ log_handlers: Default::default(),
143+ current_requests: Default::default(),
144+ pending_requests: Default::default(),
145+ };
146+ let messages = this.start_handlers(transport_pipes, cx).await?;
147+ Ok((messages, this))
148+ }
149+
150+ async fn start_handlers(
151+ &mut self,
152+ mut params: TransportPipe,
153+ cx: AsyncApp,
154+ ) -> Result<(Receiver<Message>, Sender<Message>)> {
155+ let (client_tx, server_rx) = unbounded::<Message>();
156+ let (server_tx, client_rx) = unbounded::<Message>();
157+
158+ let log_dap_communications =
159+ cx.update(|cx| DebuggerSettings::get_global(cx).log_dap_communications)
160+ .with_context(|| "Failed to get Debugger Setting log dap communications error in transport::start_handlers. Defaulting to false")
161+ .unwrap_or(false);
162+
163+ let log_handler = if log_dap_communications {
164+ Some(self.log_handlers.clone())
165+ } else {
166+ None
167+ };
168+
169+ cx.update(|cx| {
170+ if let Some(stdout) = params.stdout.take() {
171+ cx.background_executor()
172+ .spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
173+ .detach_and_log_err(cx);
174+ }
175+
176+ cx.background_executor()
177+ .spawn(Self::handle_output(
178+ params.output,
179+ client_tx,
180+ self.pending_requests.clone(),
181+ log_handler.clone(),
182+ ))
183+ .detach_and_log_err(cx);
184+
185+ if let Some(stderr) = params.stderr.take() {
186+ cx.background_executor()
187+ .spawn(Self::handle_error(stderr, self.log_handlers.clone()))
188+ .detach_and_log_err(cx);
189+ }
190+
191+ cx.background_executor()
192+ .spawn(Self::handle_input(
193+ params.input,
194+ client_rx,
195+ self.current_requests.clone(),
196+ self.pending_requests.clone(),
197+ log_handler.clone(),
198+ ))
199+ .detach_and_log_err(cx);
200+ })?;
201+
202+ {
203+ let mut lock = self.server_tx.lock().await;
204+ *lock = Some(server_tx.clone());
205+ }
206+
207+ Ok((server_rx, server_tx))
208+ }
209+
210+ pub(crate) async fn add_pending_request(
211+ &self,
212+ sequence_id: u64,
213+ request: oneshot::Sender<Result<Response>>,
214+ ) {
215+ let mut pending_requests = self.pending_requests.lock().await;
216+ pending_requests.insert(sequence_id, request);
217+ }
218+
219+ pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) {
220+ let mut pending_requests = self.pending_requests.lock().await;
221+ pending_requests.remove(sequence_id);
222+ }
223+
224+ pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
225+ if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
226+ server_tx
227+ .send(message)
228+ .await
229+ .map_err(|e| anyhow!("Failed to send message: {}", e))
230+ } else {
231+ Err(anyhow!("Server tx already dropped"))
232+ }
233+ }
234+
235+ async fn handle_adapter_log<Stdout>(
236+ stdout: Stdout,
237+ log_handlers: Option<LogHandlers>,
238+ ) -> Result<()>
239+ where
240+ Stdout: AsyncRead + Unpin + Send + 'static,
241+ {
242+ let mut reader = BufReader::new(stdout);
243+ let mut line = String::new();
244+
245+ let result = loop {
246+ line.truncate(0);
247+
248+ let bytes_read = match reader.read_line(&mut line).await {
249+ Ok(bytes_read) => bytes_read,
250+ Err(e) => break Err(e.into()),
251+ };
252+
253+ if bytes_read == 0 {
254+ break Err(anyhow!("Debugger log stream closed"));
255+ }
256+
257+ if let Some(log_handlers) = log_handlers.as_ref() {
258+ for (kind, handler) in log_handlers.lock().iter_mut() {
259+ if matches!(kind, LogKind::Adapter) {
260+ handler(IoKind::StdOut, line.as_str());
261+ }
262+ }
263+ }
264+
265+ smol::future::yield_now().await;
266+ };
267+
268+ log::debug!("Handle adapter log dropped");
269+
270+ result
271+ }
272+
273+ fn build_rpc_message(message: String) -> String {
274+ format!("Content-Length: {}\r\n\r\n{}", message.len(), message)
275+ }
276+
277+ async fn handle_input<Stdin>(
278+ mut server_stdin: Stdin,
279+ client_rx: Receiver<Message>,
280+ current_requests: Requests,
281+ pending_requests: Requests,
282+ log_handlers: Option<LogHandlers>,
283+ ) -> Result<()>
284+ where
285+ Stdin: AsyncWrite + Unpin + Send + 'static,
286+ {
287+ let result = loop {
288+ match client_rx.recv().await {
289+ Ok(message) => {
290+ if let Message::Request(request) = &message {
291+ if let Some(sender) = current_requests.lock().await.remove(&request.seq) {
292+ pending_requests.lock().await.insert(request.seq, sender);
293+ }
294+ }
295+
296+ let message = match serde_json::to_string(&message) {
297+ Ok(message) => message,
298+ Err(e) => break Err(e.into()),
299+ };
300+
301+ if let Some(log_handlers) = log_handlers.as_ref() {
302+ for (kind, log_handler) in log_handlers.lock().iter_mut() {
303+ if matches!(kind, LogKind::Rpc) {
304+ log_handler(IoKind::StdIn, &message);
305+ }
306+ }
307+ }
308+
309+ if let Err(e) = server_stdin
310+ .write_all(Self::build_rpc_message(message).as_bytes())
311+ .await
312+ {
313+ break Err(e.into());
314+ }
315+
316+ if let Err(e) = server_stdin.flush().await {
317+ break Err(e.into());
318+ }
319+ }
320+ Err(error) => break Err(error.into()),
321+ }
322+
323+ smol::future::yield_now().await;
324+ };
325+
326+ log::debug!("Handle adapter input dropped");
327+
328+ result
329+ }
330+
331+ async fn handle_output<Stdout>(
332+ server_stdout: Stdout,
333+ client_tx: Sender<Message>,
334+ pending_requests: Requests,
335+ log_handlers: Option<LogHandlers>,
336+ ) -> Result<()>
337+ where
338+ Stdout: AsyncRead + Unpin + Send + 'static,
339+ {
340+ let mut recv_buffer = String::new();
341+ let mut reader = BufReader::new(server_stdout);
342+
343+ let result = loop {
344+ let message =
345+ Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
346+ .await;
347+
348+ match message {
349+ Ok(Message::Response(res)) => {
350+ if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
351+ if let Err(e) = tx.send(Self::process_response(res)) {
352+ log::trace!("Did not send response `{:?}` for a cancelled", e);
353+ }
354+ } else {
355+ client_tx.send(Message::Response(res)).await?;
356+ };
357+ }
358+ Ok(message) => {
359+ client_tx.send(message).await?;
360+ }
361+ Err(e) => break Err(e),
362+ }
363+
364+ smol::future::yield_now().await;
365+ };
366+
367+ drop(client_tx);
368+
369+ log::debug!("Handle adapter output dropped");
370+
371+ result
372+ }
373+
374+ async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()>
375+ where
376+ Stderr: AsyncRead + Unpin + Send + 'static,
377+ {
378+ let mut buffer = String::new();
379+
380+ let mut reader = BufReader::new(stderr);
381+
382+ let result = loop {
383+ match reader.read_line(&mut buffer).await {
384+ Ok(0) => break Err(anyhow!("debugger error stream closed")),
385+ Ok(_) => {
386+ for (kind, log_handler) in log_handlers.lock().iter_mut() {
387+ if matches!(kind, LogKind::Adapter) {
388+ log_handler(IoKind::StdErr, buffer.as_str());
389+ }
390+ }
391+
392+ buffer.truncate(0);
393+ }
394+ Err(error) => break Err(error.into()),
395+ }
396+
397+ smol::future::yield_now().await;
398+ };
399+
400+ log::debug!("Handle adapter error dropped");
401+
402+ result
403+ }
404+
405+ fn process_response(response: Response) -> Result<Response> {
406+ if response.success {
407+ Ok(response)
408+ } else {
409+ if let Some(error_message) = response
410+ .body
411+ .clone()
412+ .and_then(|body| serde_json::from_value::<ErrorResponse>(body).ok())
413+ .and_then(|response| response.error.map(|msg| msg.format))
414+ .or_else(|| response.message.clone())
415+ {
416+ return Err(anyhow!(error_message));
417+ };
418+
419+ Err(anyhow!(
420+ "Received error response from adapter. Response: {:?}",
421+ response.clone()
422+ ))
423+ }
424+ }
425+
426+ async fn receive_server_message<Stdout>(
427+ reader: &mut BufReader<Stdout>,
428+ buffer: &mut String,
429+ log_handlers: Option<&LogHandlers>,
430+ ) -> Result<Message>
431+ where
432+ Stdout: AsyncRead + Unpin + Send + 'static,
433+ {
434+ let mut content_length = None;
435+ loop {
436+ buffer.truncate(0);
437+
438+ if reader
439+ .read_line(buffer)
440+ .await
441+ .with_context(|| "reading a message from server")?
442+ == 0
443+ {
444+ return Err(anyhow!("debugger reader stream closed"));
445+ };
446+
447+ if buffer == "\r\n" {
448+ break;
449+ }
450+
451+ let parts = buffer.trim().split_once(": ");
452+
453+ match parts {
454+ Some(("Content-Length", value)) => {
455+ content_length = Some(value.parse().context("invalid content length")?);
456+ }
457+ _ => {}
458+ }
459+ }
460+
461+ let content_length = content_length.context("missing content length")?;
462+
463+ let mut content = vec![0; content_length];
464+ reader
465+ .read_exact(&mut content)
466+ .await
467+ .with_context(|| "reading after a loop")?;
468+
469+ let message = std::str::from_utf8(&content).context("invalid utf8 from server")?;
470+
471+ if let Some(log_handlers) = log_handlers {
472+ for (kind, log_handler) in log_handlers.lock().iter_mut() {
473+ if matches!(kind, LogKind::Rpc) {
474+ log_handler(IoKind::StdOut, &message);
475+ }
476+ }
477+ }
478+
479+ Ok(serde_json::from_str::<Message>(message)?)
480+ }
481+
482+ pub async fn shutdown(&self) -> Result<()> {
483+ log::debug!("Start shutdown client");
484+
485+ if let Some(server_tx) = self.server_tx.lock().await.take().as_ref() {
486+ server_tx.close();
487+ }
488+
489+ let mut current_requests = self.current_requests.lock().await;
490+ let mut pending_requests = self.pending_requests.lock().await;
491+
492+ current_requests.clear();
493+ pending_requests.clear();
494+
495+ let _ = self.transport.kill().await.log_err();
496+
497+ drop(current_requests);
498+ drop(pending_requests);
499+
500+ log::debug!("Shutdown client completed");
501+
502+ anyhow::Ok(())
503+ }
504+
505+ pub fn has_adapter_logs(&self) -> bool {
506+ self.transport.has_adapter_logs()
507+ }
508+
509+ pub fn transport(&self) -> &Transport {
510+ &self.transport
511+ }
512+
513+ pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
514+ where
515+ F: 'static + Send + FnMut(IoKind, &str),
516+ {
517+ let mut log_handlers = self.log_handlers.lock();
518+ log_handlers.push((kind, Box::new(f)));
519+ }
520+}
521+
522+pub struct TcpTransport {
523+ pub port: u16,
524+ pub host: Ipv4Addr,
525+ pub timeout: u64,
526+ process: Mutex<Child>,
527+}
528+
529+impl TcpTransport {
530+ /// Get an open port to use with the tcp client when not supplied by debug config
531+ pub async fn port(host: &TCPHost) -> Result<u16> {
532+ if let Some(port) = host.port {
533+ Ok(port)
534+ } else {
535+ Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0))
536+ .await?
537+ .local_addr()?
538+ .port())
539+ }
540+ }
541+
542+ #[allow(dead_code, reason = "This is used in non test builds of Zed")]
543+ async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
544+ let Some(connection_args) = binary.connection.as_ref() else {
545+ return Err(anyhow!("No connection arguments provided"));
546+ };
547+
548+ let host = connection_args.host;
549+ let port = connection_args.port;
550+
551+ let mut command = util::command::new_smol_command(&binary.command);
552+
553+ if let Some(cwd) = &binary.cwd {
554+ command.current_dir(cwd);
555+ }
556+
557+ if let Some(args) = &binary.arguments {
558+ command.args(args);
559+ }
560+
561+ if let Some(envs) = &binary.envs {
562+ command.envs(envs);
563+ }
564+
565+ command
566+ .stdin(Stdio::null())
567+ .stdout(Stdio::piped())
568+ .stderr(Stdio::piped())
569+ .kill_on_drop(true);
570+
571+ let mut process = command
572+ .spawn()
573+ .with_context(|| "failed to start debug adapter.")?;
574+
575+ let address = SocketAddrV4::new(host, port);
576+
577+ let timeout = connection_args.timeout.unwrap_or_else(|| {
578+ cx.update(|cx| DebuggerSettings::get_global(cx).timeout)
579+ .unwrap_or(2000u64)
580+ });
581+
582+ let (rx, tx) = select! {
583+ _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => {
584+ return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port)))
585+ },
586+ result = cx.spawn(|cx| async move {
587+ loop {
588+ match TcpStream::connect(address).await {
589+ Ok(stream) => return stream.split(),
590+ Err(_) => {
591+ cx.background_executor().timer(Duration::from_millis(100)).await;
592+ }
593+ }
594+ }
595+ }).fuse() => result
596+ };
597+ log::info!(
598+ "Debug adapter has connected to TCP server {}:{}",
599+ host,
600+ port
601+ );
602+ let stdout = process.stdout.take();
603+ let stderr = process.stderr.take();
604+
605+ let this = Self {
606+ port,
607+ host,
608+ process: Mutex::new(process),
609+ timeout,
610+ };
611+
612+ let pipe = TransportPipe::new(
613+ Box::new(tx),
614+ Box::new(BufReader::new(rx)),
615+ stdout.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>),
616+ stderr.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>),
617+ );
618+
619+ Ok((pipe, this))
620+ }
621+
622+ fn has_adapter_logs(&self) -> bool {
623+ true
624+ }
625+
626+ async fn kill(&self) -> Result<()> {
627+ self.process.lock().await.kill()?;
628+
629+ Ok(())
630+ }
631+}
632+
633+pub struct StdioTransport {
634+ process: Mutex<Child>,
635+}
636+
637+impl StdioTransport {
638+ #[allow(dead_code, reason = "This is used in non test builds of Zed")]
639+ async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
640+ let mut command = util::command::new_smol_command(&binary.command);
641+
642+ if let Some(cwd) = &binary.cwd {
643+ command.current_dir(cwd);
644+ }
645+
646+ if let Some(args) = &binary.arguments {
647+ command.args(args);
648+ }
649+
650+ if let Some(envs) = &binary.envs {
651+ command.envs(envs);
652+ }
653+
654+ command
655+ .stdin(Stdio::piped())
656+ .stdout(Stdio::piped())
657+ .stderr(Stdio::piped())
658+ .kill_on_drop(true);
659+
660+ let mut process = command
661+ .spawn()
662+ .with_context(|| "failed to spawn command.")?;
663+
664+ let stdin = process
665+ .stdin
666+ .take()
667+ .ok_or_else(|| anyhow!("Failed to open stdin"))?;
668+ let stdout = process
669+ .stdout
670+ .take()
671+ .ok_or_else(|| anyhow!("Failed to open stdout"))?;
672+ let stderr = process
673+ .stderr
674+ .take()
675+ .map(|io_err| Box::new(io_err) as Box<dyn AsyncRead + Unpin + Send>);
676+
677+ if stderr.is_none() {
678+ bail!(
679+ "Failed to connect to stderr for debug adapter command {}",
680+ &binary.command
681+ );
682+ }
683+
684+ log::info!("Debug adapter has connected to stdio adapter");
685+
686+ let process = Mutex::new(process);
687+
688+ Ok((
689+ TransportPipe::new(
690+ Box::new(stdin),
691+ Box::new(BufReader::new(stdout)),
692+ None,
693+ stderr,
694+ ),
695+ Self { process },
696+ ))
697+ }
698+
699+ fn has_adapter_logs(&self) -> bool {
700+ false
701+ }
702+
703+ async fn kill(&self) -> Result<()> {
704+ self.process.lock().await.kill()?;
705+ Ok(())
706+ }
707+}
708+
709+#[cfg(any(test, feature = "test-support"))]
710+type RequestHandler = Box<
711+ dyn Send
712+ + FnMut(
713+ u64,
714+ serde_json::Value,
715+ Arc<Mutex<async_pipe::PipeWriter>>,
716+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
717+>;
718+
719+#[cfg(any(test, feature = "test-support"))]
720+type ResponseHandler = Box<dyn Send + Fn(Response)>;
721+
722+#[cfg(any(test, feature = "test-support"))]
723+pub struct FakeTransport {
724+ // for sending fake response back from adapter side
725+ request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
726+ // for reverse request responses
727+ response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
728+}
729+
730+#[cfg(any(test, feature = "test-support"))]
731+impl FakeTransport {
732+ pub async fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
733+ where
734+ F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
735+ {
736+ self.request_handlers.lock().await.insert(
737+ R::COMMAND,
738+ Box::new(
739+ move |seq, args, writer: Arc<Mutex<async_pipe::PipeWriter>>| {
740+ let response = handler(seq, serde_json::from_value(args).unwrap());
741+
742+ let message = serde_json::to_string(&Message::Response(Response {
743+ seq: seq + 1,
744+ request_seq: seq,
745+ success: response.as_ref().is_ok(),
746+ command: R::COMMAND.into(),
747+ body: util::maybe!({ serde_json::to_value(response.ok()?).ok() }),
748+ message: None,
749+ }))
750+ .unwrap();
751+
752+ let writer = writer.clone();
753+
754+ Box::pin(async move {
755+ let mut writer = writer.lock().await;
756+ writer
757+ .write_all(TransportDelegate::build_rpc_message(message).as_bytes())
758+ .await
759+ .unwrap();
760+ writer.flush().await.unwrap();
761+ })
762+ },
763+ ),
764+ );
765+ }
766+
767+ pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
768+ where
769+ F: 'static + Send + Fn(Response),
770+ {
771+ self.response_handlers
772+ .lock()
773+ .await
774+ .insert(R::COMMAND, Box::new(handler));
775+ }
776+
777+ async fn start(cx: AsyncApp) -> Result<(TransportPipe, Self)> {
778+ let this = Self {
779+ request_handlers: Arc::new(Mutex::new(HashMap::default())),
780+ response_handlers: Arc::new(Mutex::new(HashMap::default())),
781+ };
782+ use dap_types::requests::{Request, RunInTerminal, StartDebugging};
783+ use serde_json::json;
784+
785+ let (stdin_writer, stdin_reader) = async_pipe::pipe();
786+ let (stdout_writer, stdout_reader) = async_pipe::pipe();
787+
788+ let request_handlers = this.request_handlers.clone();
789+ let response_handlers = this.response_handlers.clone();
790+ let stdout_writer = Arc::new(Mutex::new(stdout_writer));
791+
792+ cx.background_executor()
793+ .spawn(async move {
794+ let mut reader = BufReader::new(stdin_reader);
795+ let mut buffer = String::new();
796+
797+ loop {
798+ let message =
799+ TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
800+ .await;
801+
802+ match message {
803+ Err(error) => {
804+ break anyhow!(error);
805+ }
806+ Ok(message) => {
807+ match message {
808+ Message::Request(request) => {
809+ // redirect reverse requests to stdout writer/reader
810+ if request.command == RunInTerminal::COMMAND
811+ || request.command == StartDebugging::COMMAND
812+ {
813+ let message =
814+ serde_json::to_string(&Message::Request(request))
815+ .unwrap();
816+
817+ let mut writer = stdout_writer.lock().await;
818+ writer
819+ .write_all(
820+ TransportDelegate::build_rpc_message(message)
821+ .as_bytes(),
822+ )
823+ .await
824+ .unwrap();
825+ writer.flush().await.unwrap();
826+ } else {
827+ if let Some(handle) = request_handlers
828+ .lock()
829+ .await
830+ .get_mut(request.command.as_str())
831+ {
832+ handle(
833+ request.seq,
834+ request.arguments.unwrap_or(json!({})),
835+ stdout_writer.clone(),
836+ )
837+ .await;
838+ } else {
839+ log::error!(
840+ "No request handler for {}",
841+ request.command
842+ );
843+ }
844+ }
845+ }
846+ Message::Event(event) => {
847+ let message =
848+ serde_json::to_string(&Message::Event(event)).unwrap();
849+
850+ let mut writer = stdout_writer.lock().await;
851+ writer
852+ .write_all(
853+ TransportDelegate::build_rpc_message(message)
854+ .as_bytes(),
855+ )
856+ .await
857+ .unwrap();
858+ writer.flush().await.unwrap();
859+ }
860+ Message::Response(response) => {
861+ if let Some(handle) = response_handlers
862+ .lock()
863+ .await
864+ .get(response.command.as_str())
865+ {
866+ handle(response);
867+ } else {
868+ log::error!("No response handler for {}", response.command);
869+ }
870+ }
871+ }
872+ }
873+ }
874+ }
875+ })
876+ .detach();
877+
878+ Ok((
879+ TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None),
880+ this,
881+ ))
882+ }
883+
884+ fn has_adapter_logs(&self) -> bool {
885+ false
886+ }
887+
888+ async fn kill(&self) -> Result<()> {
889+ Ok(())
890+ }
891+}
crates/dap_adapters/Cargo.toml+41−0
@@ -0,0 +1,41 @@
1+[package]
2+name = "dap_adapters"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+
8+[features]
9+test-support = [
10+ "dap/test-support",
11+ "gpui/test-support",
12+ "task/test-support",
13+ "util/test-support",
14+]
15+
16+[lints]
17+workspace = true
18+
19+[lib]
20+path = "src/dap_adapters.rs"
21+doctest = false
22+
23+[dependencies]
24+anyhow.workspace = true
25+async-trait.workspace = true
26+dap.workspace = true
27+gpui.workspace = true
28+language.workspace = true
29+paths.workspace = true
30+regex.workspace = true
31+serde.workspace = true
32+serde_json.workspace = true
33+sysinfo.workspace = true
34+task.workspace = true
35+util.workspace = true
36+
37+[dev-dependencies]
38+dap = { workspace = true, features = ["test-support"] }
39+gpui = { workspace = true, features = ["test-support"] }
40+task = { workspace = true, features = ["test-support"] }
41+util = { workspace = true, features = ["test-support"] }
crates/dap_adapters/LICENSE-GPL+1−0
@@ -0,0 +1 @@
1+../../LICENSE-GPL
crates/dap_adapters/src/custom.rs+84−0
@@ -0,0 +1,84 @@
1+use dap::transport::TcpTransport;
2+use gpui::AsyncApp;
3+use serde_json::Value;
4+use std::{collections::HashMap, ffi::OsString, path::PathBuf};
5+use sysinfo::{Pid, Process};
6+use task::DebugAdapterConfig;
7+
8+use crate::*;
9+
10+pub(crate) struct CustomDebugAdapter {
11+ custom_args: CustomArgs,
12+}
13+
14+impl CustomDebugAdapter {
15+ const ADAPTER_NAME: &'static str = "custom_dap";
16+
17+ pub(crate) async fn new(custom_args: CustomArgs) -> Result<Self> {
18+ Ok(CustomDebugAdapter { custom_args })
19+ }
20+
21+ pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
22+ processes.iter().collect::<Vec<_>>()
23+ }
24+}
25+
26+#[async_trait(?Send)]
27+impl DebugAdapter for CustomDebugAdapter {
28+ fn name(&self) -> DebugAdapterName {
29+ DebugAdapterName(Self::ADAPTER_NAME.into())
30+ }
31+
32+ async fn get_binary(
33+ &self,
34+ _: &dyn DapDelegate,
35+ config: &DebugAdapterConfig,
36+ _: Option<PathBuf>,
37+ _: &mut AsyncApp,
38+ ) -> Result<DebugAdapterBinary> {
39+ let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection
40+ {
41+ Some(adapters::TcpArguments {
42+ host: connection.host(),
43+ port: TcpTransport::port(&connection).await?,
44+ timeout: connection.timeout,
45+ })
46+ } else {
47+ None
48+ };
49+ let ret = DebugAdapterBinary {
50+ command: self.custom_args.command.clone(),
51+ arguments: self
52+ .custom_args
53+ .args
54+ .clone()
55+ .map(|args| args.iter().map(OsString::from).collect()),
56+ cwd: config.cwd.clone(),
57+ envs: self.custom_args.envs.clone(),
58+ connection,
59+ };
60+ Ok(ret)
61+ }
62+
63+ async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
64+ bail!("Custom debug adapters don't have latest versions")
65+ }
66+
67+ async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> {
68+ bail!("Custom debug adapters cannot be installed")
69+ }
70+
71+ async fn get_installed_binary(
72+ &self,
73+ _: &dyn DapDelegate,
74+ _: &DebugAdapterConfig,
75+ _: Option<PathBuf>,
76+ _: &mut AsyncApp,
77+ ) -> Result<DebugAdapterBinary> {
78+ bail!("Custom debug adapters cannot be installed")
79+ }
80+
81+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
82+ json!({"program": config.program})
83+ }
84+}
crates/dap_adapters/src/dap_adapters.rs+67−0
@@ -0,0 +1,67 @@
1+mod custom;
2+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
3+mod gdb;
4+mod go;
5+mod javascript;
6+mod lldb;
7+mod php;
8+mod python;
9+
10+use std::{collections::HashMap, sync::Arc};
11+
12+use anyhow::{anyhow, bail, Result};
13+use async_trait::async_trait;
14+use custom::CustomDebugAdapter;
15+use dap::adapters::{
16+ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
17+ GithubRepo,
18+};
19+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
20+use gdb::GdbDebugAdapter;
21+use go::GoDebugAdapter;
22+use javascript::JsDebugAdapter;
23+use lldb::LldbDebugAdapter;
24+use php::PhpDebugAdapter;
25+use python::PythonDebugAdapter;
26+use serde_json::{json, Value};
27+use sysinfo::{Pid, Process};
28+use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
29+
30+pub async fn build_adapter(kind: &DebugAdapterKind) -> Result<Arc<dyn DebugAdapter>> {
31+ match kind {
32+ DebugAdapterKind::Custom(start_args) => {
33+ Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?))
34+ }
35+ DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)),
36+ DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)),
37+ DebugAdapterKind::Javascript(host) => {
38+ Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?))
39+ }
40+ DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())),
41+ DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)),
42+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
43+ DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())),
44+ #[cfg(any(test, feature = "test-support"))]
45+ DebugAdapterKind::Fake(_) => Ok(Arc::new(dap::adapters::FakeAdapter::new())),
46+ #[cfg(not(any(test, feature = "test-support")))]
47+ #[allow(unreachable_patterns)]
48+ _ => unreachable!("Fake variant only exists with test-support feature"),
49+ }
50+}
51+
52+pub fn attach_processes<'a>(
53+ kind: &DebugAdapterKind,
54+ processes: &'a HashMap<Pid, Process>,
55+) -> Vec<(&'a Pid, &'a Process)> {
56+ match kind {
57+ #[cfg(any(test, feature = "test-support"))]
58+ DebugAdapterKind::Fake(_) => processes
59+ .iter()
60+ .filter(|(pid, _)| pid.as_u32() == std::process::id())
61+ .collect::<Vec<_>>(),
62+ DebugAdapterKind::Custom(_) => CustomDebugAdapter::attach_processes(processes),
63+ DebugAdapterKind::Javascript(_) => JsDebugAdapter::attach_processes(processes),
64+ DebugAdapterKind::Lldb => LldbDebugAdapter::attach_processes(processes),
65+ _ => processes.iter().collect::<Vec<_>>(),
66+ }
67+}
crates/dap_adapters/src/gdb.rs+83−0
@@ -0,0 +1,83 @@
1+use std::ffi::OsStr;
2+
3+use anyhow::Result;
4+use async_trait::async_trait;
5+use gpui::AsyncApp;
6+use task::DebugAdapterConfig;
7+
8+use crate::*;
9+
10+pub(crate) struct GdbDebugAdapter {}
11+
12+impl GdbDebugAdapter {
13+ const ADAPTER_NAME: &'static str = "gdb";
14+
15+ pub(crate) fn new() -> Self {
16+ GdbDebugAdapter {}
17+ }
18+}
19+
20+#[async_trait(?Send)]
21+impl DebugAdapter for GdbDebugAdapter {
22+ fn name(&self) -> DebugAdapterName {
23+ DebugAdapterName(Self::ADAPTER_NAME.into())
24+ }
25+
26+ async fn get_binary(
27+ &self,
28+ delegate: &dyn DapDelegate,
29+ config: &DebugAdapterConfig,
30+ user_installed_path: Option<std::path::PathBuf>,
31+ _: &mut AsyncApp,
32+ ) -> Result<DebugAdapterBinary> {
33+ let user_setting_path = user_installed_path
34+ .filter(|p| p.exists())
35+ .and_then(|p| p.to_str().map(|s| s.to_string()));
36+
37+ /* GDB implements DAP natively so just need to */
38+ let gdb_path = delegate
39+ .which(OsStr::new("gdb"))
40+ .and_then(|p| p.to_str().map(|s| s.to_string()))
41+ .ok_or(anyhow!("Could not find gdb in path"));
42+
43+ if gdb_path.is_err() && user_setting_path.is_none() {
44+ bail!("Could not find gdb path or it's not installed");
45+ }
46+
47+ let gdb_path = user_setting_path.unwrap_or(gdb_path?);
48+
49+ Ok(DebugAdapterBinary {
50+ command: gdb_path,
51+ arguments: Some(vec!["-i=dap".into()]),
52+ envs: None,
53+ cwd: config.cwd.clone(),
54+ connection: None,
55+ })
56+ }
57+
58+ async fn install_binary(
59+ &self,
60+ _version: AdapterVersion,
61+ _delegate: &dyn DapDelegate,
62+ ) -> Result<()> {
63+ unimplemented!("GDB debug adapter cannot be installed by Zed (yet)")
64+ }
65+
66+ async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
67+ unimplemented!("Fetch latest GDB version not implemented (yet)")
68+ }
69+
70+ async fn get_installed_binary(
71+ &self,
72+ _: &dyn DapDelegate,
73+ _: &DebugAdapterConfig,
74+ _: Option<std::path::PathBuf>,
75+ _: &mut AsyncApp,
76+ ) -> Result<DebugAdapterBinary> {
77+ unimplemented!("GDB cannot be installed by Zed (yet)")
78+ }
79+
80+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
81+ json!({"program": config.program, "cwd": config.cwd})
82+ }
83+}
crates/dap_adapters/src/go.rs+100−0
@@ -0,0 +1,100 @@
1+use dap::transport::TcpTransport;
2+use gpui::AsyncApp;
3+use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
4+
5+use crate::*;
6+
7+pub(crate) struct GoDebugAdapter {
8+ port: u16,
9+ host: Ipv4Addr,
10+ timeout: Option<u64>,
11+}
12+
13+impl GoDebugAdapter {
14+ const ADAPTER_NAME: &'static str = "delve";
15+
16+ pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
17+ Ok(GoDebugAdapter {
18+ port: TcpTransport::port(host).await?,
19+ host: host.host(),
20+ timeout: host.timeout,
21+ })
22+ }
23+}
24+
25+#[async_trait(?Send)]
26+impl DebugAdapter for GoDebugAdapter {
27+ fn name(&self) -> DebugAdapterName {
28+ DebugAdapterName(Self::ADAPTER_NAME.into())
29+ }
30+
31+ async fn get_binary(
32+ &self,
33+ delegate: &dyn DapDelegate,
34+ config: &DebugAdapterConfig,
35+ user_installed_path: Option<PathBuf>,
36+ cx: &mut AsyncApp,
37+ ) -> Result<DebugAdapterBinary> {
38+ self.get_installed_binary(delegate, config, user_installed_path, cx)
39+ .await
40+ }
41+
42+ async fn fetch_latest_adapter_version(
43+ &self,
44+ _delegate: &dyn DapDelegate,
45+ ) -> Result<AdapterVersion> {
46+ unimplemented!("This adapter is used from path for now");
47+ }
48+
49+ async fn install_binary(
50+ &self,
51+ version: AdapterVersion,
52+ delegate: &dyn DapDelegate,
53+ ) -> Result<()> {
54+ adapters::download_adapter_from_github(
55+ self.name(),
56+ version,
57+ adapters::DownloadedFileType::Zip,
58+ delegate,
59+ )
60+ .await?;
61+ Ok(())
62+ }
63+
64+ async fn get_installed_binary(
65+ &self,
66+ delegate: &dyn DapDelegate,
67+ config: &DebugAdapterConfig,
68+ _: Option<PathBuf>,
69+ _: &mut AsyncApp,
70+ ) -> Result<DebugAdapterBinary> {
71+ let delve_path = delegate
72+ .which(OsStr::new("dlv"))
73+ .and_then(|p| p.to_str().map(|p| p.to_string()))
74+ .ok_or(anyhow!("Dlv not found in path"))?;
75+
76+ Ok(DebugAdapterBinary {
77+ command: delve_path,
78+ arguments: Some(vec![
79+ "dap".into(),
80+ "--listen".into(),
81+ format!("{}:{}", self.host, self.port).into(),
82+ ]),
83+ cwd: config.cwd.clone(),
84+ envs: None,
85+ connection: Some(adapters::TcpArguments {
86+ host: self.host,
87+ port: self.port,
88+ timeout: self.timeout,
89+ }),
90+ })
91+ }
92+
93+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
94+ json!({
95+ "program": config.program,
96+ "cwd": config.cwd,
97+ "subProcess": true,
98+ })
99+ }
100+}
crates/dap_adapters/src/javascript.rs+148−0
@@ -0,0 +1,148 @@
1+use adapters::latest_github_release;
2+use dap::transport::TcpTransport;
3+use gpui::AsyncApp;
4+use regex::Regex;
5+use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf};
6+use sysinfo::{Pid, Process};
7+use task::DebugRequestType;
8+
9+use crate::*;
10+
11+pub(crate) struct JsDebugAdapter {
12+ port: u16,
13+ host: Ipv4Addr,
14+ timeout: Option<u64>,
15+}
16+
17+impl JsDebugAdapter {
18+ const ADAPTER_NAME: &'static str = "vscode-js-debug";
19+ const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
20+
21+ pub(crate) async fn new(host: TCPHost) -> Result<Self> {
22+ Ok(JsDebugAdapter {
23+ host: host.host(),
24+ timeout: host.timeout,
25+ port: TcpTransport::port(&host).await?,
26+ })
27+ }
28+
29+ pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
30+ let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap();
31+
32+ processes
33+ .iter()
34+ .filter(|(_, process)| regex.is_match(&process.name().to_string_lossy()))
35+ .collect::<Vec<_>>()
36+ }
37+}
38+
39+#[async_trait(?Send)]
40+impl DebugAdapter for JsDebugAdapter {
41+ fn name(&self) -> DebugAdapterName {
42+ DebugAdapterName(Self::ADAPTER_NAME.into())
43+ }
44+
45+ async fn fetch_latest_adapter_version(
46+ &self,
47+ delegate: &dyn DapDelegate,
48+ ) -> Result<AdapterVersion> {
49+ let release = latest_github_release(
50+ &format!("{}/{}", "microsoft", Self::ADAPTER_NAME),
51+ true,
52+ false,
53+ delegate.http_client(),
54+ )
55+ .await?;
56+
57+ let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
58+
59+ Ok(AdapterVersion {
60+ tag_name: release.tag_name,
61+ url: release
62+ .assets
63+ .iter()
64+ .find(|asset| asset.name == asset_name)
65+ .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
66+ .browser_download_url
67+ .clone(),
68+ })
69+ }
70+
71+ async fn get_installed_binary(
72+ &self,
73+ delegate: &dyn DapDelegate,
74+ config: &DebugAdapterConfig,
75+ user_installed_path: Option<PathBuf>,
76+ _: &mut AsyncApp,
77+ ) -> Result<DebugAdapterBinary> {
78+ let adapter_path = if let Some(user_installed_path) = user_installed_path {
79+ user_installed_path
80+ } else {
81+ let adapter_path = paths::debug_adapters_dir().join(self.name());
82+
83+ let file_name_prefix = format!("{}_", self.name());
84+
85+ util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
86+ file_name.starts_with(&file_name_prefix)
87+ })
88+ .await
89+ .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
90+ };
91+
92+ Ok(DebugAdapterBinary {
93+ command: delegate
94+ .node_runtime()
95+ .binary_path()
96+ .await?
97+ .to_string_lossy()
98+ .into_owned(),
99+ arguments: Some(vec![
100+ adapter_path.join(Self::ADAPTER_PATH).into(),
101+ self.port.to_string().into(),
102+ self.host.to_string().into(),
103+ ]),
104+ cwd: config.cwd.clone(),
105+ envs: None,
106+ connection: Some(adapters::TcpArguments {
107+ host: self.host,
108+ port: self.port,
109+ timeout: self.timeout,
110+ }),
111+ })
112+ }
113+
114+ async fn install_binary(
115+ &self,
116+ version: AdapterVersion,
117+ delegate: &dyn DapDelegate,
118+ ) -> Result<()> {
119+ adapters::download_adapter_from_github(
120+ self.name(),
121+ version,
122+ adapters::DownloadedFileType::GzipTar,
123+ delegate,
124+ )
125+ .await?;
126+
127+ return Ok(());
128+ }
129+
130+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
131+ let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
132+ attach_config.process_id
133+ } else {
134+ None
135+ };
136+
137+ json!({
138+ "program": config.program,
139+ "type": "pwa-node",
140+ "request": match config.request {
141+ DebugRequestType::Launch => "launch",
142+ DebugRequestType::Attach(_) => "attach",
143+ },
144+ "processId": pid,
145+ "cwd": config.cwd,
146+ })
147+ }
148+}
crates/dap_adapters/src/lldb.rs+104−0
@@ -0,0 +1,104 @@
1+use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
2+
3+use anyhow::Result;
4+use async_trait::async_trait;
5+use gpui::AsyncApp;
6+use sysinfo::{Pid, Process};
7+use task::{DebugAdapterConfig, DebugRequestType};
8+
9+use crate::*;
10+
11+pub(crate) struct LldbDebugAdapter {}
12+
13+impl LldbDebugAdapter {
14+ const ADAPTER_NAME: &'static str = "lldb";
15+
16+ pub(crate) fn new() -> Self {
17+ LldbDebugAdapter {}
18+ }
19+
20+ pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> {
21+ processes.iter().collect::<Vec<_>>()
22+ }
23+}
24+
25+#[async_trait(?Send)]
26+impl DebugAdapter for LldbDebugAdapter {
27+ fn name(&self) -> DebugAdapterName {
28+ DebugAdapterName(Self::ADAPTER_NAME.into())
29+ }
30+
31+ async fn get_binary(
32+ &self,
33+ delegate: &dyn DapDelegate,
34+ config: &DebugAdapterConfig,
35+ user_installed_path: Option<PathBuf>,
36+ _: &mut AsyncApp,
37+ ) -> Result<DebugAdapterBinary> {
38+ let lldb_dap_path = if let Some(user_installed_path) = user_installed_path {
39+ user_installed_path.to_string_lossy().into()
40+ } else if cfg!(target_os = "macos") {
41+ util::command::new_smol_command("xcrun")
42+ .args(&["-f", "lldb-dap"])
43+ .output()
44+ .await
45+ .ok()
46+ .and_then(|output| String::from_utf8(output.stdout).ok())
47+ .map(|path| path.trim().to_string())
48+ .ok_or(anyhow!("Failed to find lldb-dap in user's path"))?
49+ } else {
50+ delegate
51+ .which(OsStr::new("lldb-dap"))
52+ .and_then(|p| p.to_str().map(|s| s.to_string()))
53+ .ok_or(anyhow!("Could not find lldb-dap in path"))?
54+ };
55+
56+ Ok(DebugAdapterBinary {
57+ command: lldb_dap_path,
58+ arguments: None,
59+ envs: None,
60+ cwd: config.cwd.clone(),
61+ connection: None,
62+ })
63+ }
64+
65+ async fn install_binary(
66+ &self,
67+ _version: AdapterVersion,
68+ _delegate: &dyn DapDelegate,
69+ ) -> Result<()> {
70+ unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
71+ }
72+
73+ async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
74+ unimplemented!("Fetch latest adapter version not implemented for lldb (yet)")
75+ }
76+
77+ async fn get_installed_binary(
78+ &self,
79+ _: &dyn DapDelegate,
80+ _: &DebugAdapterConfig,
81+ _: Option<PathBuf>,
82+ _: &mut AsyncApp,
83+ ) -> Result<DebugAdapterBinary> {
84+ unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
85+ }
86+
87+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
88+ let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
89+ attach_config.process_id
90+ } else {
91+ None
92+ };
93+
94+ json!({
95+ "program": config.program,
96+ "request": match config.request {
97+ DebugRequestType::Launch => "launch",
98+ DebugRequestType::Attach(_) => "attach",
99+ },
100+ "pid": pid,
101+ "cwd": config.cwd,
102+ })
103+ }
104+}
crates/dap_adapters/src/php.rs+123−0
@@ -0,0 +1,123 @@
1+use adapters::latest_github_release;
2+use dap::{adapters::TcpArguments, transport::TcpTransport};
3+use gpui::AsyncApp;
4+use std::{net::Ipv4Addr, path::PathBuf};
5+
6+use crate::*;
7+
8+pub(crate) struct PhpDebugAdapter {
9+ port: u16,
10+ host: Ipv4Addr,
11+ timeout: Option<u64>,
12+}
13+
14+impl PhpDebugAdapter {
15+ const ADAPTER_NAME: &'static str = "vscode-php-debug";
16+ const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
17+
18+ pub(crate) async fn new(host: TCPHost) -> Result<Self> {
19+ Ok(PhpDebugAdapter {
20+ port: TcpTransport::port(&host).await?,
21+ host: host.host(),
22+ timeout: host.timeout,
23+ })
24+ }
25+}
26+
27+#[async_trait(?Send)]
28+impl DebugAdapter for PhpDebugAdapter {
29+ fn name(&self) -> DebugAdapterName {
30+ DebugAdapterName(Self::ADAPTER_NAME.into())
31+ }
32+
33+ async fn fetch_latest_adapter_version(
34+ &self,
35+ delegate: &dyn DapDelegate,
36+ ) -> Result<AdapterVersion> {
37+ let release = latest_github_release(
38+ &format!("{}/{}", "xdebug", Self::ADAPTER_NAME),
39+ true,
40+ false,
41+ delegate.http_client(),
42+ )
43+ .await?;
44+
45+ let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
46+
47+ Ok(AdapterVersion {
48+ tag_name: release.tag_name,
49+ url: release
50+ .assets
51+ .iter()
52+ .find(|asset| asset.name == asset_name)
53+ .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
54+ .browser_download_url
55+ .clone(),
56+ })
57+ }
58+
59+ async fn get_installed_binary(
60+ &self,
61+ delegate: &dyn DapDelegate,
62+ config: &DebugAdapterConfig,
63+ user_installed_path: Option<PathBuf>,
64+ _: &mut AsyncApp,
65+ ) -> Result<DebugAdapterBinary> {
66+ let adapter_path = if let Some(user_installed_path) = user_installed_path {
67+ user_installed_path
68+ } else {
69+ let adapter_path = paths::debug_adapters_dir().join(self.name());
70+
71+ let file_name_prefix = format!("{}_", self.name());
72+
73+ util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
74+ file_name.starts_with(&file_name_prefix)
75+ })
76+ .await
77+ .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
78+ };
79+
80+ Ok(DebugAdapterBinary {
81+ command: delegate
82+ .node_runtime()
83+ .binary_path()
84+ .await?
85+ .to_string_lossy()
86+ .into_owned(),
87+ arguments: Some(vec![
88+ adapter_path.join(Self::ADAPTER_PATH).into(),
89+ format!("--server={}", self.port).into(),
90+ ]),
91+ connection: Some(TcpArguments {
92+ port: self.port,
93+ host: self.host,
94+ timeout: self.timeout,
95+ }),
96+ cwd: config.cwd.clone(),
97+ envs: None,
98+ })
99+ }
100+
101+ async fn install_binary(
102+ &self,
103+ version: AdapterVersion,
104+ delegate: &dyn DapDelegate,
105+ ) -> Result<()> {
106+ adapters::download_adapter_from_github(
107+ self.name(),
108+ version,
109+ adapters::DownloadedFileType::Vsix,
110+ delegate,
111+ )
112+ .await?;
113+
114+ Ok(())
115+ }
116+
117+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
118+ json!({
119+ "program": config.program,
120+ "cwd": config.cwd,
121+ })
122+ }
123+}
crates/dap_adapters/src/python.rs+142−0
@@ -0,0 +1,142 @@
1+use crate::*;
2+use dap::transport::TcpTransport;
3+use gpui::AsyncApp;
4+use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
5+
6+pub(crate) struct PythonDebugAdapter {
7+ port: u16,
8+ host: Ipv4Addr,
9+ timeout: Option<u64>,
10+}
11+
12+impl PythonDebugAdapter {
13+ const ADAPTER_NAME: &'static str = "debugpy";
14+ const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
15+ const LANGUAGE_NAME: &'static str = "Python";
16+
17+ pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
18+ Ok(PythonDebugAdapter {
19+ port: TcpTransport::port(host).await?,
20+ host: host.host(),
21+ timeout: host.timeout,
22+ })
23+ }
24+}
25+
26+#[async_trait(?Send)]
27+impl DebugAdapter for PythonDebugAdapter {
28+ fn name(&self) -> DebugAdapterName {
29+ DebugAdapterName(Self::ADAPTER_NAME.into())
30+ }
31+
32+ async fn fetch_latest_adapter_version(
33+ &self,
34+ delegate: &dyn DapDelegate,
35+ ) -> Result<AdapterVersion> {
36+ let github_repo = GithubRepo {
37+ repo_name: Self::ADAPTER_NAME.into(),
38+ repo_owner: "microsoft".into(),
39+ };
40+
41+ adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
42+ }
43+
44+ async fn install_binary(
45+ &self,
46+ version: AdapterVersion,
47+ delegate: &dyn DapDelegate,
48+ ) -> Result<()> {
49+ let version_path = adapters::download_adapter_from_github(
50+ self.name(),
51+ version,
52+ adapters::DownloadedFileType::Zip,
53+ delegate,
54+ )
55+ .await?;
56+
57+ // only needed when you install the latest version for the first time
58+ if let Some(debugpy_dir) =
59+ util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
60+ file_name.starts_with("microsoft-debugpy-")
61+ })
62+ .await
63+ {
64+ // TODO Debugger: Rename folder instead of moving all files to another folder
65+ // We're doing unnecessary IO work right now
66+ util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
67+ .await?;
68+ }
69+
70+ Ok(())
71+ }
72+
73+ async fn get_installed_binary(
74+ &self,
75+ delegate: &dyn DapDelegate,
76+ config: &DebugAdapterConfig,
77+ user_installed_path: Option<PathBuf>,
78+ cx: &mut AsyncApp,
79+ ) -> Result<DebugAdapterBinary> {
80+ const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
81+
82+ let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
83+ user_installed_path
84+ } else {
85+ let adapter_path = paths::debug_adapters_dir().join(self.name());
86+ let file_name_prefix = format!("{}_", self.name());
87+
88+ util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
89+ file_name.starts_with(&file_name_prefix)
90+ })
91+ .await
92+ .ok_or_else(|| anyhow!("Debugpy directory not found"))?
93+ };
94+
95+ let toolchain = delegate
96+ .toolchain_store()
97+ .active_toolchain(
98+ delegate.worktree_id(),
99+ language::LanguageName::new(Self::LANGUAGE_NAME),
100+ cx,
101+ )
102+ .await;
103+
104+ let python_path = if let Some(toolchain) = toolchain {
105+ Some(toolchain.path.to_string())
106+ } else {
107+ BINARY_NAMES
108+ .iter()
109+ .filter_map(|cmd| {
110+ delegate
111+ .which(OsStr::new(cmd))
112+ .map(|path| path.to_string_lossy().to_string())
113+ })
114+ .find(|_| true)
115+ };
116+
117+ Ok(DebugAdapterBinary {
118+ command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
119+ arguments: Some(vec![
120+ debugpy_dir.join(Self::ADAPTER_PATH).into(),
121+ format!("--port={}", self.port).into(),
122+ format!("--host={}", self.host).into(),
123+ ]),
124+ connection: Some(adapters::TcpArguments {
125+ host: self.host,
126+ port: self.port,
127+ timeout: self.timeout,
128+ }),
129+ cwd: config.cwd.clone(),
130+ envs: None,
131+ })
132+ }
133+
134+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
135+ json!({
136+ "program": config.program,
137+ "subProcess": true,
138+ "cwd": config.cwd,
139+ "redirectOutput": true,
140+ })
141+ }
142+}
crates/debugger_tools/Cargo.toml+26−0
@@ -0,0 +1,26 @@
1+[package]
2+name = "debugger_tools"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+
8+[lints]
9+workspace = true
10+
11+[lib]
12+path = "src/debugger_tools.rs"
13+doctest = false
14+
15+[dependencies]
16+anyhow.workspace = true
17+dap.workspace = true
18+editor.workspace = true
19+futures.workspace = true
20+gpui.workspace = true
21+project.workspace = true
22+serde_json.workspace = true
23+settings.workspace = true
24+smol.workspace = true
25+util.workspace = true
26+workspace.workspace = true
crates/debugger_tools/LICENSE-GPL+1−0
@@ -0,0 +1 @@
1+../../LICENSE-GPL
crates/debugger_tools/src/dap_log.rs+845−0
@@ -0,0 +1,845 @@
1+use dap::{
2+ client::SessionId,
3+ debugger_settings::DebuggerSettings,
4+ transport::{IoKind, LogKind},
5+};
6+use editor::{Editor, EditorEvent};
7+use futures::{
8+ channel::mpsc::{unbounded, UnboundedSender},
9+ StreamExt,
10+};
11+use gpui::{
12+ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable,
13+ IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
14+};
15+use project::{
16+ debugger::{dap_store, session::Session},
17+ search::SearchQuery,
18+ Project,
19+};
20+use settings::Settings as _;
21+use std::{
22+ borrow::Cow,
23+ collections::{HashMap, VecDeque},
24+ sync::Arc,
25+};
26+use util::maybe;
27+use workspace::{
28+ item::Item,
29+ searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
30+ ui::{h_flex, Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu},
31+ ToolbarItemEvent, ToolbarItemView, Workspace,
32+};
33+
34+struct DapLogView {
35+ editor: Entity<Editor>,
36+ focus_handle: FocusHandle,
37+ log_store: Entity<LogStore>,
38+ editor_subscriptions: Vec<Subscription>,
39+ current_view: Option<(SessionId, LogKind)>,
40+ project: Entity<Project>,
41+ _subscriptions: Vec<Subscription>,
42+}
43+
44+struct LogStore {
45+ projects: HashMap<WeakEntity<Project>, ProjectState>,
46+ debug_clients: HashMap<SessionId, DebugAdapterState>,
47+ rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
48+ adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
49+}
50+
51+struct ProjectState {
52+ _subscriptions: [gpui::Subscription; 2],
53+}
54+
55+struct DebugAdapterState {
56+ log_messages: VecDeque<String>,
57+ rpc_messages: RpcMessages,
58+}
59+
60+struct RpcMessages {
61+ messages: VecDeque<String>,
62+ last_message_kind: Option<MessageKind>,
63+}
64+
65+impl RpcMessages {
66+ const MESSAGE_QUEUE_LIMIT: usize = 255;
67+
68+ fn new() -> Self {
69+ Self {
70+ last_message_kind: None,
71+ messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
72+ }
73+ }
74+}
75+
76+const SEND: &str = "// Send";
77+const RECEIVE: &str = "// Receive";
78+
79+#[derive(Clone, Copy, PartialEq, Eq)]
80+enum MessageKind {
81+ Send,
82+ Receive,
83+}
84+
85+impl MessageKind {
86+ fn label(&self) -> &'static str {
87+ match self {
88+ Self::Send => SEND,
89+ Self::Receive => RECEIVE,
90+ }
91+ }
92+}
93+
94+impl DebugAdapterState {
95+ fn new() -> Self {
96+ Self {
97+ log_messages: VecDeque::new(),
98+ rpc_messages: RpcMessages::new(),
99+ }
100+ }
101+}
102+
103+impl LogStore {
104+ fn new(cx: &Context<Self>) -> Self {
105+ let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
106+ cx.spawn(|this, mut cx| async move {
107+ while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
108+ if let Some(this) = this.upgrade() {
109+ this.update(&mut cx, |this, cx| {
110+ this.on_rpc_log(client_id, io_kind, &message, cx);
111+ })?;
112+ }
113+
114+ smol::future::yield_now().await;
115+ }
116+ anyhow::Ok(())
117+ })
118+ .detach_and_log_err(cx);
119+
120+ let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();
121+ cx.spawn(|this, mut cx| async move {
122+ while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
123+ if let Some(this) = this.upgrade() {
124+ this.update(&mut cx, |this, cx| {
125+ this.on_adapter_log(client_id, io_kind, &message, cx);
126+ })?;
127+ }
128+
129+ smol::future::yield_now().await;
130+ }
131+ anyhow::Ok(())
132+ })
133+ .detach_and_log_err(cx);
134+ Self {
135+ rpc_tx,
136+ adapter_log_tx,
137+ projects: HashMap::new(),
138+ debug_clients: HashMap::new(),
139+ }
140+ }
141+
142+ fn on_rpc_log(
143+ &mut self,
144+ client_id: SessionId,
145+ io_kind: IoKind,
146+ message: &str,
147+ cx: &mut Context<Self>,
148+ ) {
149+ self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
150+ }
151+
152+ fn on_adapter_log(
153+ &mut self,
154+ client_id: SessionId,
155+ io_kind: IoKind,
156+ message: &str,
157+ cx: &mut Context<Self>,
158+ ) {
159+ self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
160+ }
161+
162+ pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
163+ let weak_project = project.downgrade();
164+ self.projects.insert(
165+ project.downgrade(),
166+ ProjectState {
167+ _subscriptions: [
168+ cx.observe_release(project, move |this, _, _| {
169+ this.projects.remove(&weak_project);
170+ }),
171+ cx.subscribe(
172+ &project.read(cx).dap_store(),
173+ |this, dap_store, event, cx| match event {
174+ dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
175+ let session = dap_store.read(cx).session_by_id(session_id);
176+ if let Some(session) = session {
177+ this.add_debug_client(*session_id, session, cx);
178+ }
179+ }
180+ dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
181+ this.remove_debug_client(*session_id, cx);
182+ }
183+
184+ _ => {}
185+ },
186+ ),
187+ ],
188+ },
189+ );
190+ }
191+
192+ fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
193+ self.debug_clients.get_mut(&id)
194+ }
195+
196+ fn add_debug_client_message(
197+ &mut self,
198+ id: SessionId,
199+ io_kind: IoKind,
200+ message: String,
201+ cx: &mut Context<Self>,
202+ ) {
203+ let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
204+ return;
205+ };
206+
207+ let kind = match io_kind {
208+ IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
209+ IoKind::StdIn => MessageKind::Send,
210+ };
211+
212+ let rpc_messages = &mut debug_client_state.rpc_messages;
213+ if rpc_messages.last_message_kind != Some(kind) {
214+ Self::add_debug_client_entry(
215+ &mut rpc_messages.messages,
216+ id,
217+ kind.label().to_string(),
218+ LogKind::Rpc,
219+ cx,
220+ );
221+ rpc_messages.last_message_kind = Some(kind);
222+ }
223+ Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
224+
225+ cx.notify();
226+ }
227+
228+ fn add_debug_client_log(
229+ &mut self,
230+ id: SessionId,
231+ io_kind: IoKind,
232+ message: String,
233+ cx: &mut Context<Self>,
234+ ) {
235+ let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
236+ return;
237+ };
238+
239+ let message = match io_kind {
240+ IoKind::StdErr => {
241+ let mut message = message.clone();
242+ message.insert_str(0, "stderr: ");
243+ message
244+ }
245+ _ => message,
246+ };
247+
248+ Self::add_debug_client_entry(
249+ &mut debug_client_state.log_messages,
250+ id,
251+ message,
252+ LogKind::Adapter,
253+ cx,
254+ );
255+ cx.notify();
256+ }
257+
258+ fn add_debug_client_entry(
259+ log_lines: &mut VecDeque<String>,
260+ id: SessionId,
261+ message: String,
262+ kind: LogKind,
263+ cx: &mut Context<Self>,
264+ ) {
265+ while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
266+ log_lines.pop_front();
267+ }
268+
269+ let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
270+
271+ let entry = if format_messages {
272+ maybe!({
273+ serde_json::to_string_pretty::<serde_json::Value>(
274+ &serde_json::from_str(&message).ok()?,
275+ )
276+ .ok()
277+ })
278+ .unwrap_or(message)
279+ } else {
280+ message
281+ };
282+ log_lines.push_back(entry.clone());
283+
284+ cx.emit(Event::NewLogEntry { id, entry, kind });
285+ }
286+
287+ fn add_debug_client(
288+ &mut self,
289+ client_id: SessionId,
290+ client: Entity<Session>,
291+ cx: &App,
292+ ) -> Option<&mut DebugAdapterState> {
293+ let client_state = self
294+ .debug_clients
295+ .entry(client_id)
296+ .or_insert_with(DebugAdapterState::new);
297+
298+ let io_tx = self.rpc_tx.clone();
299+
300+ let client = client.read(cx).adapter_client()?;
301+ client.add_log_handler(
302+ move |io_kind, message| {
303+ io_tx
304+ .unbounded_send((client_id, io_kind, message.to_string()))
305+ .ok();
306+ },
307+ LogKind::Rpc,
308+ );
309+
310+ let log_io_tx = self.adapter_log_tx.clone();
311+ client.add_log_handler(
312+ move |io_kind, message| {
313+ log_io_tx
314+ .unbounded_send((client_id, io_kind, message.to_string()))
315+ .ok();
316+ },
317+ LogKind::Adapter,
318+ );
319+
320+ Some(client_state)
321+ }
322+
323+ fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
324+ self.debug_clients.remove(&client_id);
325+ cx.notify();
326+ }
327+
328+ fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
329+ Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
330+ }
331+
332+ fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
333+ Some(
334+ &mut self
335+ .debug_clients
336+ .get_mut(&client_id)?
337+ .rpc_messages
338+ .messages,
339+ )
340+ }
341+}
342+
343+pub struct DapLogToolbarItemView {
344+ log_view: Option<Entity<DapLogView>>,
345+}
346+
347+impl DapLogToolbarItemView {
348+ pub fn new() -> Self {
349+ Self { log_view: None }
350+ }
351+}
352+
353+impl Render for DapLogToolbarItemView {
354+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
355+ let Some(log_view) = self.log_view.clone() else {
356+ return Empty.into_any_element();
357+ };
358+
359+ let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {
360+ (
361+ log_view.menu_items(cx).unwrap_or_default(),
362+ log_view.current_view.map(|(client_id, _)| client_id),
363+ )
364+ });
365+
366+ let current_client = current_client_id.and_then(|current_client_id| {
367+ menu_rows
368+ .iter()
369+ .find(|row| row.client_id == current_client_id)
370+ });
371+
372+ let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
373+ .anchor(gpui::Corner::TopLeft)
374+ .trigger(Button::new(
375+ "debug_client_menu_header",
376+ current_client
377+ .map(|sub_item| {
378+ Cow::Owned(format!(
379+ "{} ({}) - {}",
380+ sub_item.client_name,
381+ sub_item.client_id.0,
382+ match sub_item.selected_entry {
383+ LogKind::Adapter => ADAPTER_LOGS,
384+ LogKind::Rpc => RPC_MESSAGES,
385+ }
386+ ))
387+ })
388+ .unwrap_or_else(|| "No adapter selected".into()),
389+ ))
390+ .menu(move |mut window, cx| {
391+ let log_view = log_view.clone();
392+ let menu_rows = menu_rows.clone();
393+ ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
394+ for row in menu_rows.into_iter() {
395+ menu = menu.custom_row(move |_window, _cx| {
396+ div()
397+ .w_full()
398+ .pl_2()
399+ .child(
400+ Label::new(
401+ format!("{}. {}", row.client_id.0, row.client_name,),
402+ )
403+ .color(workspace::ui::Color::Muted),
404+ )
405+ .into_any_element()
406+ });
407+
408+ if row.has_adapter_logs {
409+ menu = menu.custom_entry(
410+ move |_window, _cx| {
411+ div()
412+ .w_full()
413+ .pl_4()
414+ .child(Label::new(ADAPTER_LOGS))
415+ .into_any_element()
416+ },
417+ window.handler_for(&log_view, move |view, window, cx| {
418+ view.show_log_messages_for_adapter(row.client_id, window, cx);
419+ }),
420+ );
421+ }
422+
423+ menu = menu.custom_entry(
424+ move |_window, _cx| {
425+ div()
426+ .w_full()
427+ .pl_4()
428+ .child(Label::new(RPC_MESSAGES))
429+ .into_any_element()
430+ },
431+ window.handler_for(&log_view, move |view, window, cx| {
432+ view.show_rpc_trace_for_server(row.client_id, window, cx);
433+ }),
434+ );
435+ }
436+
437+ menu
438+ })
439+ .into()
440+ });
441+
442+ h_flex()
443+ .size_full()
444+ .child(dap_menu)
445+ .child(
446+ div()
447+ .child(
448+ Button::new("clear_log_button", "Clear").on_click(cx.listener(
449+ |this, _, window, cx| {
450+ if let Some(log_view) = this.log_view.as_ref() {
451+ log_view.update(cx, |log_view, cx| {
452+ log_view.editor.update(cx, |editor, cx| {
453+ editor.set_read_only(false);
454+ editor.clear(window, cx);
455+ editor.set_read_only(true);
456+ });
457+ })
458+ }
459+ },
460+ )),
461+ )
462+ .ml_2(),
463+ )
464+ .into_any_element()
465+ }
466+}
467+
468+impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
469+
470+impl ToolbarItemView for DapLogToolbarItemView {
471+ fn set_active_pane_item(
472+ &mut self,
473+ active_pane_item: Option<&dyn workspace::item::ItemHandle>,
474+ _window: &mut Window,
475+ cx: &mut Context<Self>,
476+ ) -> workspace::ToolbarItemLocation {
477+ if let Some(item) = active_pane_item {
478+ if let Some(log_view) = item.downcast::<DapLogView>() {
479+ self.log_view = Some(log_view.clone());
480+ return workspace::ToolbarItemLocation::PrimaryLeft;
481+ }
482+ }
483+ self.log_view = None;
484+
485+ cx.notify();
486+
487+ workspace::ToolbarItemLocation::Hidden
488+ }
489+}
490+
491+impl DapLogView {
492+ pub fn new(
493+ project: Entity<Project>,
494+ log_store: Entity<LogStore>,
495+ window: &mut Window,
496+ cx: &mut Context<Self>,
497+ ) -> Self {
498+ let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
499+
500+ let focus_handle = cx.focus_handle();
501+
502+ let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
503+ Event::NewLogEntry { id, entry, kind } => {
504+ if log_view.current_view == Some((*id, *kind)) {
505+ log_view.editor.update(cx, |editor, cx| {
506+ editor.set_read_only(false);
507+ let last_point = editor.buffer().read(cx).len(cx);
508+ editor.edit(
509+ vec![
510+ (last_point..last_point, entry.trim()),
511+ (last_point..last_point, "\n"),
512+ ],
513+ cx,
514+ );
515+ editor.set_read_only(true);
516+ });
517+ }
518+ }
519+ });
520+
521+ Self {
522+ editor,
523+ focus_handle,
524+ project,
525+ log_store,
526+ editor_subscriptions,
527+ current_view: None,
528+ _subscriptions: vec![events_subscriptions],
529+ }
530+ }
531+
532+ fn editor_for_logs(
533+ log_contents: String,
534+ window: &mut Window,
535+ cx: &mut Context<Self>,
536+ ) -> (Entity<Editor>, Vec<Subscription>) {
537+ let editor = cx.new(|cx| {
538+ let mut editor = Editor::multi_line(window, cx);
539+ editor.set_text(log_contents, window, cx);
540+ editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
541+ editor.set_show_code_actions(false, cx);
542+ editor.set_show_breakpoints(false, cx);
543+ editor.set_show_git_diff_gutter(false, cx);
544+ editor.set_show_runnables(false, cx);
545+ editor.set_input_enabled(false);
546+ editor.set_use_autoclose(false);
547+ editor.set_read_only(true);
548+ editor.set_show_edit_predictions(Some(false), window, cx);
549+ editor
550+ });
551+ let editor_subscription = cx.subscribe(
552+ &editor,
553+ |_, _, event: &EditorEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()),
554+ );
555+ let search_subscription = cx.subscribe(
556+ &editor,
557+ |_, _, event: &SearchEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()),
558+ );
559+ (editor, vec![editor_subscription, search_subscription])
560+ }
561+
562+ fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
563+ let mut menu_items = self
564+ .project
565+ .read(cx)
566+ .dap_store()
567+ .read(cx)
568+ .sessions()
569+ .filter_map(|client| {
570+ let client = client.read(cx).adapter_client()?;
571+ Some(DapMenuItem {
572+ client_id: client.id(),
573+ client_name: client.name().0.as_ref().into(),
574+ has_adapter_logs: client.has_adapter_logs(),
575+ selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
576+ })
577+ })
578+ .collect::<Vec<_>>();
579+ menu_items.sort_by_key(|item| item.client_id.0);
580+ Some(menu_items)
581+ }
582+
583+ fn show_rpc_trace_for_server(
584+ &mut self,
585+ client_id: SessionId,
586+ window: &mut Window,
587+ cx: &mut Context<Self>,
588+ ) {
589+ let rpc_log = self.log_store.update(cx, |log_store, _| {
590+ log_store
591+ .rpc_messages_for_client(client_id)
592+ .map(|state| log_contents(&state))
593+ });
594+ if let Some(rpc_log) = rpc_log {
595+ self.current_view = Some((client_id, LogKind::Rpc));
596+ let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
597+ let language = self.project.read(cx).languages().language_for_name("JSON");
598+ editor
599+ .read(cx)
600+ .buffer()
601+ .read(cx)
602+ .as_singleton()
603+ .expect("log buffer should be a singleton")
604+ .update(cx, |_, cx| {
605+ cx.spawn({
606+ let buffer = cx.entity();
607+ |_, mut cx| async move {
608+ let language = language.await.ok();
609+ buffer.update(&mut cx, |buffer, cx| {
610+ buffer.set_language(language, cx);
611+ })
612+ }
613+ })
614+ .detach_and_log_err(cx);
615+ });
616+
617+ self.editor = editor;
618+ self.editor_subscriptions = editor_subscriptions;
619+ cx.notify();
620+ }
621+
622+ cx.focus_self(window);
623+ }
624+
625+ fn show_log_messages_for_adapter(
626+ &mut self,
627+ client_id: SessionId,
628+ window: &mut Window,
629+ cx: &mut Context<Self>,
630+ ) {
631+ let message_log = self.log_store.update(cx, |log_store, _| {
632+ log_store
633+ .log_messages_for_client(client_id)
634+ .map(|state| log_contents(&state))
635+ });
636+ if let Some(message_log) = message_log {
637+ self.current_view = Some((client_id, LogKind::Adapter));
638+ let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
639+ editor
640+ .read(cx)
641+ .buffer()
642+ .read(cx)
643+ .as_singleton()
644+ .expect("log buffer should be a singleton");
645+
646+ self.editor = editor;
647+ self.editor_subscriptions = editor_subscriptions;
648+ cx.notify();
649+ }
650+
651+ cx.focus_self(window);
652+ }
653+}
654+
655+fn log_contents(lines: &VecDeque<String>) -> String {
656+ let (a, b) = lines.as_slices();
657+ let a = a.iter().map(move |v| v.as_ref());
658+ let b = b.iter().map(move |v| v.as_ref());
659+ a.chain(b).fold(String::new(), |mut acc, el| {
660+ acc.push_str(el);
661+ acc.push('\n');
662+ acc
663+ })
664+}
665+
666+#[derive(Clone, PartialEq)]
667+pub(crate) struct DapMenuItem {
668+ pub client_id: SessionId,
669+ pub client_name: String,
670+ pub has_adapter_logs: bool,
671+ pub selected_entry: LogKind,
672+}
673+
674+const ADAPTER_LOGS: &str = "Adapter Logs";
675+const RPC_MESSAGES: &str = "RPC Messages";
676+
677+impl Render for DapLogView {
678+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
679+ self.editor.update(cx, |editor, cx| {
680+ editor.render(window, cx).into_any_element()
681+ })
682+ }
683+}
684+
685+actions!(debug, [OpenDebuggerAdapterLogs]);
686+
687+pub fn init(cx: &mut App) {
688+ let log_store = cx.new(|cx| LogStore::new(cx));
689+
690+ cx.observe_new(move |workspace: &mut Workspace, window, cx| {
691+ let Some(_window) = window else {
692+ return;
693+ };
694+
695+ let project = workspace.project();
696+ if project.read(cx).is_local() {
697+ log_store.update(cx, |store, cx| {
698+ store.add_project(project, cx);
699+ });
700+ }
701+
702+ let log_store = log_store.clone();
703+ workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| {
704+ let project = workspace.project().read(cx);
705+ if project.is_local() {
706+ workspace.add_item_to_active_pane(
707+ Box::new(cx.new(|cx| {
708+ DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
709+ })),
710+ None,
711+ true,
712+ window,
713+ cx,
714+ );
715+ }
716+ });
717+ })
718+ .detach();
719+}
720+
721+impl Item for DapLogView {
722+ type Event = EditorEvent;
723+
724+ fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
725+ Editor::to_item_events(event, f)
726+ }
727+
728+ fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
729+ Some("DAP Logs".into())
730+ }
731+
732+ fn telemetry_event_text(&self) -> Option<&'static str> {
733+ None
734+ }
735+
736+ fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
737+ Some(Box::new(handle.clone()))
738+ }
739+}
740+
741+impl SearchableItem for DapLogView {
742+ type Match = <Editor as SearchableItem>::Match;
743+
744+ fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
745+ self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
746+ }
747+
748+ fn update_matches(
749+ &mut self,
750+ matches: &[Self::Match],
751+ window: &mut Window,
752+ cx: &mut Context<Self>,
753+ ) {
754+ self.editor
755+ .update(cx, |e, cx| e.update_matches(matches, window, cx))
756+ }
757+
758+ fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
759+ self.editor
760+ .update(cx, |e, cx| e.query_suggestion(window, cx))
761+ }
762+
763+ fn activate_match(
764+ &mut self,
765+ index: usize,
766+ matches: &[Self::Match],
767+ window: &mut Window,
768+ cx: &mut Context<Self>,
769+ ) {
770+ self.editor
771+ .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
772+ }
773+
774+ fn select_matches(
775+ &mut self,
776+ matches: &[Self::Match],
777+ window: &mut Window,
778+ cx: &mut Context<Self>,
779+ ) {
780+ self.editor
781+ .update(cx, |e, cx| e.select_matches(matches, window, cx))
782+ }
783+
784+ fn find_matches(
785+ &mut self,
786+ query: Arc<project::search::SearchQuery>,
787+ window: &mut Window,
788+ cx: &mut Context<Self>,
789+ ) -> gpui::Task<Vec<Self::Match>> {
790+ self.editor
791+ .update(cx, |e, cx| e.find_matches(query, window, cx))
792+ }
793+
794+ fn replace(
795+ &mut self,
796+ _: &Self::Match,
797+ _: &SearchQuery,
798+ _window: &mut Window,
799+ _: &mut Context<Self>,
800+ ) {
801+ // Since DAP Log is read-only, it doesn't make sense to support replace operation.
802+ }
803+
804+ fn supported_options(&self) -> workspace::searchable::SearchOptions {
805+ workspace::searchable::SearchOptions {
806+ case: true,
807+ word: true,
808+ regex: true,
809+ find_in_results: true,
810+ // DAP log is read-only.
811+ replacement: false,
812+ selection: false,
813+ }
814+ }
815+ fn active_match_index(
816+ &mut self,
817+ direction: Direction,
818+ matches: &[Self::Match],
819+ window: &mut Window,
820+ cx: &mut Context<Self>,
821+ ) -> Option<usize> {
822+ self.editor.update(cx, |e, cx| {
823+ e.active_match_index(direction, matches, window, cx)
824+ })
825+ }
826+}
827+
828+impl Focusable for DapLogView {
829+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
830+ self.focus_handle.clone()
831+ }
832+}
833+
834+pub enum Event {
835+ NewLogEntry {
836+ id: SessionId,
837+ entry: String,
838+ kind: LogKind,
839+ },
840+}
841+
842+impl EventEmitter<Event> for LogStore {}
843+impl EventEmitter<Event> for DapLogView {}
844+impl EventEmitter<EditorEvent> for DapLogView {}
845+impl EventEmitter<SearchEvent> for DapLogView {}
crates/debugger_tools/src/debugger_tools.rs+8−0
@@ -0,0 +1,8 @@
1+mod dap_log;
2+pub use dap_log::*;
3+
4+use gpui::App;
5+
6+pub fn init(cx: &mut App) {
7+ dap_log::init(cx);
8+}
crates/debugger_ui/Cargo.toml+58−0
@@ -0,0 +1,58 @@
1+[package]
2+name = "debugger_ui"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+
8+[lints]
9+workspace = true
10+
11+[features]
12+test-support = [
13+ "dap/test-support",
14+ "editor/test-support",
15+ "gpui/test-support",
16+ "project/test-support",
17+ "util/test-support",
18+ "workspace/test-support",
19+]
20+
21+[dependencies]
22+anyhow.workspace = true
23+client.workspace = true
24+collections.workspace = true
25+command_palette_hooks.workspace = true
26+dap.workspace = true
27+editor.workspace = true
28+feature_flags.workspace = true
29+futures.workspace = true
30+fuzzy.workspace = true
31+gpui.workspace = true
32+language.workspace = true
33+log.workspace = true
34+menu.workspace = true
35+picker.workspace = true
36+pretty_assertions.workspace = true
37+project.workspace = true
38+rpc.workspace = true
39+serde.workspace = true
40+serde_json.workspace = true
41+settings.workspace = true
42+sysinfo.workspace = true
43+task.workspace = true
44+terminal_view.workspace = true
45+theme.workspace = true
46+ui.workspace = true
47+util.workspace = true
48+workspace.workspace = true
49+
50+[dev-dependencies]
51+dap = { workspace = true, features = ["test-support"] }
52+editor = { workspace = true, features = ["test-support"] }
53+env_logger.workspace = true
54+gpui = { workspace = true, features = ["test-support"] }
55+project = { workspace = true, features = ["test-support"] }
56+unindent.workspace = true
57+util = { workspace = true, features = ["test-support"] }
58+workspace = { workspace = true, features = ["test-support"] }
crates/debugger_ui/LICENSE-GPL+1−0
@@ -0,0 +1 @@
1+../../LICENSE-GPL
crates/debugger_ui/src/attach_modal.rs+293−0
@@ -0,0 +1,293 @@
1+use dap::DebugRequestType;
2+use fuzzy::{StringMatch, StringMatchCandidate};
3+use gpui::Subscription;
4+use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
5+use picker::{Picker, PickerDelegate};
6+use project::debugger::attach_processes;
7+
8+use std::sync::Arc;
9+use sysinfo::System;
10+use ui::{prelude::*, Context, Tooltip};
11+use ui::{ListItem, ListItemSpacing};
12+use util::debug_panic;
13+use workspace::ModalView;
14+
15+#[derive(Debug, Clone)]
16+struct Candidate {
17+ pid: u32,
18+ name: String,
19+ command: Vec<String>,
20+}
21+
22+pub(crate) struct AttachModalDelegate {
23+ selected_index: usize,
24+ matches: Vec<StringMatch>,
25+ placeholder_text: Arc<str>,
26+ project: Entity<project::Project>,
27+ debug_config: task::DebugAdapterConfig,
28+ candidates: Option<Vec<Candidate>>,
29+}
30+
31+impl AttachModalDelegate {
32+ pub fn new(project: Entity<project::Project>, debug_config: task::DebugAdapterConfig) -> Self {
33+ Self {
34+ project,
35+ debug_config,
36+ candidates: None,
37+ selected_index: 0,
38+ matches: Vec::default(),
39+ placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
40+ }
41+ }
42+}
43+
44+pub struct AttachModal {
45+ _subscription: Subscription,
46+ pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
47+}
48+
49+impl AttachModal {
50+ pub fn new(
51+ project: Entity<project::Project>,
52+ debug_config: task::DebugAdapterConfig,
53+ window: &mut Window,
54+ cx: &mut Context<Self>,
55+ ) -> Self {
56+ let picker = cx.new(|cx| {
57+ Picker::uniform_list(AttachModalDelegate::new(project, debug_config), window, cx)
58+ });
59+ Self {
60+ _subscription: cx.subscribe(&picker, |_, _, _, cx| {
61+ cx.emit(DismissEvent);
62+ }),
63+ picker,
64+ }
65+ }
66+}
67+
68+impl Render for AttachModal {
69+ fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
70+ v_flex()
71+ .key_context("AttachModal")
72+ .w(rems(34.))
73+ .child(self.picker.clone())
74+ }
75+}
76+
77+impl EventEmitter<DismissEvent> for AttachModal {}
78+
79+impl Focusable for AttachModal {
80+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
81+ self.picker.read(cx).focus_handle(cx)
82+ }
83+}
84+
85+impl ModalView for AttachModal {}
86+
87+impl PickerDelegate for AttachModalDelegate {
88+ type ListItem = ListItem;
89+
90+ fn match_count(&self) -> usize {
91+ self.matches.len()
92+ }
93+
94+ fn selected_index(&self) -> usize {
95+ self.selected_index
96+ }
97+
98+ fn set_selected_index(
99+ &mut self,
100+ ix: usize,
101+ _window: &mut Window,
102+ _: &mut Context<Picker<Self>>,
103+ ) {
104+ self.selected_index = ix;
105+ }
106+
107+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
108+ self.placeholder_text.clone()
109+ }
110+
111+ fn update_matches(
112+ &mut self,
113+ query: String,
114+ _window: &mut Window,
115+ cx: &mut Context<Picker<Self>>,
116+ ) -> gpui::Task<()> {
117+ cx.spawn(|this, mut cx| async move {
118+ let Some(processes) = this
119+ .update(&mut cx, |this, _| {
120+ if let Some(processes) = this.delegate.candidates.clone() {
121+ processes
122+ } else {
123+ let system = System::new_all();
124+
125+ let processes =
126+ attach_processes(&this.delegate.debug_config.kind, &system.processes());
127+ let candidates = processes
128+ .into_iter()
129+ .map(|(pid, process)| Candidate {
130+ pid: pid.as_u32(),
131+ name: process.name().to_string_lossy().into_owned(),
132+ command: process
133+ .cmd()
134+ .iter()
135+ .map(|s| s.to_string_lossy().to_string())
136+ .collect::<Vec<_>>(),
137+ })
138+ .collect::<Vec<Candidate>>();
139+
140+ let _ = this.delegate.candidates.insert(candidates.clone());
141+
142+ candidates
143+ }
144+ })
145+ .ok()
146+ else {
147+ return;
148+ };
149+
150+ let matches = fuzzy::match_strings(
151+ &processes
152+ .iter()
153+ .enumerate()
154+ .map(|(id, candidate)| {
155+ StringMatchCandidate::new(
156+ id,
157+ format!(
158+ "{} {} {}",
159+ candidate.command.join(" "),
160+ candidate.pid,
161+ candidate.name
162+ )
163+ .as_str(),
164+ )
165+ })
166+ .collect::<Vec<_>>(),
167+ &query,
168+ true,
169+ 100,
170+ &Default::default(),
171+ cx.background_executor().clone(),
172+ )
173+ .await;
174+
175+ this.update(&mut cx, |this, _| {
176+ let delegate = &mut this.delegate;
177+
178+ delegate.matches = matches;
179+ delegate.candidates = Some(processes);
180+
181+ if delegate.matches.is_empty() {
182+ delegate.selected_index = 0;
183+ } else {
184+ delegate.selected_index =
185+ delegate.selected_index.min(delegate.matches.len() - 1);
186+ }
187+ })
188+ .ok();
189+ })
190+ }
191+
192+ fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
193+ let candidate = self
194+ .matches
195+ .get(self.selected_index())
196+ .and_then(|current_match| {
197+ let ix = current_match.candidate_id;
198+ self.candidates.as_ref().map(|candidates| &candidates[ix])
199+ });
200+
201+ let Some(candidate) = candidate else {
202+ return cx.emit(DismissEvent);
203+ };
204+
205+ match &mut self.debug_config.request {
206+ DebugRequestType::Attach(config) => {
207+ config.process_id = Some(candidate.pid);
208+ }
209+ DebugRequestType::Launch => {
210+ debug_panic!("Debugger attach modal used on launch debug config");
211+ return;
212+ }
213+ }
214+
215+ let config = self.debug_config.clone();
216+ self.project
217+ .update(cx, |project, cx| project.start_debug_session(config, cx))
218+ .detach_and_log_err(cx);
219+
220+ cx.emit(DismissEvent);
221+ }
222+
223+ fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
224+ self.selected_index = 0;
225+ self.candidates.take();
226+
227+ cx.emit(DismissEvent);
228+ }
229+
230+ fn render_match(
231+ &self,
232+ ix: usize,
233+ selected: bool,
234+ _window: &mut Window,
235+ _: &mut Context<Picker<Self>>,
236+ ) -> Option<Self::ListItem> {
237+ let candidates = self.candidates.as_ref()?;
238+ let hit = &self.matches[ix];
239+ let candidate = &candidates.get(hit.candidate_id)?;
240+
241+ Some(
242+ ListItem::new(SharedString::from(format!("process-entry-{ix}")))
243+ .inset(true)
244+ .spacing(ListItemSpacing::Sparse)
245+ .toggle_state(selected)
246+ .child(
247+ v_flex()
248+ .items_start()
249+ .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
250+ .child(
251+ div()
252+ .id(SharedString::from(format!("process-entry-{ix}-command")))
253+ .tooltip(Tooltip::text(
254+ candidate
255+ .command
256+ .clone()
257+ .into_iter()
258+ .collect::<Vec<_>>()
259+ .join(" "),
260+ ))
261+ .child(
262+ Label::new(format!(
263+ "{} {}",
264+ candidate.name,
265+ candidate
266+ .command
267+ .clone()
268+ .into_iter()
269+ .skip(1)
270+ .collect::<Vec<_>>()
271+ .join(" ")
272+ ))
273+ .size(LabelSize::Small)
274+ .color(Color::Muted),
275+ ),
276+ ),
277+ ),
278+ )
279+ }
280+}
281+
282+#[allow(dead_code)]
283+#[cfg(any(test, feature = "test-support"))]
284+pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
285+ modal.picker.update(cx, |picker, _| {
286+ picker
287+ .delegate
288+ .matches
289+ .iter()
290+ .map(|hit| hit.string.clone())
291+ .collect::<Vec<_>>()
292+ })
293+}
crates/debugger_ui/src/debugger_panel.rs+536−0
@@ -0,0 +1,536 @@
1+use crate::session::DebugSession;
2+use anyhow::{anyhow, Result};
3+use collections::HashMap;
4+use command_palette_hooks::CommandPaletteFilter;
5+use dap::{
6+ client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
7+ ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
8+};
9+use futures::{channel::mpsc, SinkExt as _};
10+use gpui::{
11+ actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
12+ Focusable, Subscription, Task, WeakEntity,
13+};
14+use project::{
15+ debugger::dap_store::{self, DapStore},
16+ terminals::TerminalKind,
17+ Project,
18+};
19+use rpc::proto::{self};
20+use settings::Settings;
21+use std::{any::TypeId, path::PathBuf};
22+use terminal_view::terminal_panel::TerminalPanel;
23+use ui::prelude::*;
24+use workspace::{
25+ dock::{DockPosition, Panel, PanelEvent},
26+ pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
27+ ToggleIgnoreBreakpoints, Workspace,
28+};
29+
30+pub enum DebugPanelEvent {
31+ Exited(SessionId),
32+ Terminated(SessionId),
33+ Stopped {
34+ client_id: SessionId,
35+ event: StoppedEvent,
36+ go_to_stack_frame: bool,
37+ },
38+ Thread((SessionId, ThreadEvent)),
39+ Continued((SessionId, ContinuedEvent)),
40+ Output((SessionId, OutputEvent)),
41+ Module((SessionId, ModuleEvent)),
42+ LoadedSource((SessionId, LoadedSourceEvent)),
43+ ClientShutdown(SessionId),
44+ CapabilitiesChanged(SessionId),
45+}
46+
47+actions!(debug_panel, [ToggleFocus]);
48+pub struct DebugPanel {
49+ size: Pixels,
50+ pane: Entity<Pane>,
51+ project: WeakEntity<Project>,
52+ workspace: WeakEntity<Workspace>,
53+ _subscriptions: Vec<Subscription>,
54+}
55+
56+impl DebugPanel {
57+ pub fn new(
58+ workspace: &Workspace,
59+ window: &mut Window,
60+ cx: &mut Context<Workspace>,
61+ ) -> Entity<Self> {
62+ cx.new(|cx| {
63+ let project = workspace.project().clone();
64+ let dap_store = project.read(cx).dap_store();
65+ let weak_workspace = workspace.weak_handle();
66+ let pane = cx.new(|cx| {
67+ let mut pane = Pane::new(
68+ workspace.weak_handle(),
69+ project.clone(),
70+ Default::default(),
71+ None,
72+ gpui::NoAction.boxed_clone(),
73+ window,
74+ cx,
75+ );
76+ pane.set_can_split(None);
77+ pane.set_can_navigate(true, cx);
78+ pane.display_nav_history_buttons(None);
79+ pane.set_should_display_tab_bar(|_window, _cx| true);
80+ pane.set_close_pane_if_empty(true, cx);
81+ pane.set_render_tab_bar_buttons(cx, {
82+ let project = project.clone();
83+ let weak_workspace = weak_workspace.clone();
84+ move |_, _, cx| {
85+ let project = project.clone();
86+ let weak_workspace = weak_workspace.clone();
87+ (
88+ None,
89+ Some(
90+ h_flex()
91+ .child(
92+ IconButton::new("new-debug-session", IconName::Plus)
93+ .icon_size(IconSize::Small)
94+ .on_click(cx.listener(move |pane, _, window, cx| {
95+ pane.add_item(
96+ Box::new(DebugSession::inert(
97+ project.clone(),
98+ weak_workspace.clone(),
99+ window,
100+ cx,
101+ )),
102+ false,
103+ false,
104+ None,
105+ window,
106+ cx,
107+ );
108+ })),
109+ )
110+ .into_any_element(),
111+ ),
112+ )
113+ }
114+ });
115+ pane.add_item(
116+ Box::new(DebugSession::inert(
117+ project.clone(),
118+ weak_workspace.clone(),
119+ window,
120+ cx,
121+ )),
122+ false,
123+ false,
124+ None,
125+ window,
126+ cx,
127+ );
128+ pane
129+ });
130+
131+ let _subscriptions = vec![
132+ cx.observe(&pane, |_, _, cx| cx.notify()),
133+ cx.subscribe_in(&pane, window, Self::handle_pane_event),
134+ cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
135+ ];
136+
137+ let debug_panel = Self {
138+ pane,
139+ size: px(300.),
140+ _subscriptions,
141+ project: project.downgrade(),
142+ workspace: workspace.weak_handle(),
143+ };
144+
145+ debug_panel
146+ })
147+ }
148+
149+ pub fn load(
150+ workspace: WeakEntity<Workspace>,
151+ cx: AsyncWindowContext,
152+ ) -> Task<Result<Entity<Self>>> {
153+ cx.spawn(|mut cx| async move {
154+ workspace.update_in(&mut cx, |workspace, window, cx| {
155+ let debug_panel = DebugPanel::new(workspace, window, cx);
156+
157+ cx.observe(&debug_panel, |_, debug_panel, cx| {
158+ let (has_active_session, supports_restart, support_step_back) = debug_panel
159+ .update(cx, |this, cx| {
160+ this.active_session(cx)
161+ .map(|item| {
162+ let running = item.read(cx).mode().as_running().cloned();
163+
164+ match running {
165+ Some(running) => {
166+ let caps = running.read(cx).capabilities(cx);
167+ (
168+ true,
169+ caps.supports_restart_request.unwrap_or_default(),
170+ caps.supports_step_back.unwrap_or_default(),
171+ )
172+ }
173+ None => (false, false, false),
174+ }
175+ })
176+ .unwrap_or((false, false, false))
177+ });
178+
179+ let filter = CommandPaletteFilter::global_mut(cx);
180+ let debugger_action_types = [
181+ TypeId::of::<Continue>(),
182+ TypeId::of::<StepOver>(),
183+ TypeId::of::<StepInto>(),
184+ TypeId::of::<StepOut>(),
185+ TypeId::of::<Stop>(),
186+ TypeId::of::<Disconnect>(),
187+ TypeId::of::<Pause>(),
188+ TypeId::of::<ToggleIgnoreBreakpoints>(),
189+ ];
190+
191+ let step_back_action_type = [TypeId::of::<StepBack>()];
192+ let restart_action_type = [TypeId::of::<Restart>()];
193+
194+ if has_active_session {
195+ filter.show_action_types(debugger_action_types.iter());
196+
197+ if supports_restart {
198+ filter.show_action_types(restart_action_type.iter());
199+ } else {
200+ filter.hide_action_types(&restart_action_type);
201+ }
202+
203+ if support_step_back {
204+ filter.show_action_types(step_back_action_type.iter());
205+ } else {
206+ filter.hide_action_types(&step_back_action_type);
207+ }
208+ } else {
209+ // show only the `debug: start`
210+ filter.hide_action_types(&debugger_action_types);
211+ filter.hide_action_types(&step_back_action_type);
212+ filter.hide_action_types(&restart_action_type);
213+ }
214+ })
215+ .detach();
216+
217+ debug_panel
218+ })
219+ })
220+ }
221+
222+ pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
223+ self.pane
224+ .read(cx)
225+ .active_item()
226+ .and_then(|panel| panel.downcast::<DebugSession>())
227+ }
228+
229+ pub fn debug_panel_items_by_client(
230+ &self,
231+ client_id: &SessionId,
232+ cx: &Context<Self>,
233+ ) -> Vec<Entity<DebugSession>> {
234+ self.pane
235+ .read(cx)
236+ .items()
237+ .filter_map(|item| item.downcast::<DebugSession>())
238+ .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
239+ .map(|item| item.clone())
240+ .collect()
241+ }
242+
243+ pub fn debug_panel_item_by_client(
244+ &self,
245+ client_id: SessionId,
246+ cx: &mut Context<Self>,
247+ ) -> Option<Entity<DebugSession>> {
248+ self.pane
249+ .read(cx)
250+ .items()
251+ .filter_map(|item| item.downcast::<DebugSession>())
252+ .find(|item| {
253+ let item = item.read(cx);
254+
255+ item.session_id(cx) == Some(client_id)
256+ })
257+ }
258+
259+ fn handle_dap_store_event(
260+ &mut self,
261+ dap_store: &Entity<DapStore>,
262+ event: &dap_store::DapStoreEvent,
263+ window: &mut Window,
264+ cx: &mut Context<Self>,
265+ ) {
266+ match event {
267+ dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
268+ let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
269+ return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
270+ };
271+
272+ let Some(project) = self.project.upgrade() else {
273+ return log::error!("Debug Panel out lived it's weak reference to Project");
274+ };
275+
276+ if self.pane.read_with(cx, |pane, cx| {
277+ pane.items_of_type::<DebugSession>()
278+ .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
279+ }) {
280+ // We already have an item for this session.
281+ return;
282+ }
283+ let session_item =
284+ DebugSession::running(project, self.workspace.clone(), session, window, cx);
285+
286+ self.pane.update(cx, |pane, cx| {
287+ pane.add_item(Box::new(session_item), true, true, None, window, cx);
288+ window.focus(&pane.focus_handle(cx));
289+ cx.notify();
290+ });
291+ }
292+ dap_store::DapStoreEvent::RunInTerminal {
293+ title,
294+ cwd,
295+ command,
296+ args,
297+ envs,
298+ sender,
299+ ..
300+ } => {
301+ self.handle_run_in_terminal_request(
302+ title.clone(),
303+ cwd.clone(),
304+ command.clone(),
305+ args.clone(),
306+ envs.clone(),
307+ sender.clone(),
308+ window,
309+ cx,
310+ )
311+ .detach_and_log_err(cx);
312+ }
313+ _ => {}
314+ }
315+ }
316+
317+ fn handle_run_in_terminal_request(
318+ &self,
319+ title: Option<String>,
320+ cwd: PathBuf,
321+ command: Option<String>,
322+ args: Vec<String>,
323+ envs: HashMap<String, String>,
324+ mut sender: mpsc::Sender<Result<u32>>,
325+ window: &mut Window,
326+ cx: &mut App,
327+ ) -> Task<Result<()>> {
328+ let terminal_task = self.workspace.update(cx, |workspace, cx| {
329+ let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
330+ anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
331+ });
332+
333+ let terminal_panel = match terminal_panel {
334+ Ok(panel) => panel,
335+ Err(err) => return Task::ready(Err(err)),
336+ };
337+
338+ terminal_panel.update(cx, |terminal_panel, cx| {
339+ let terminal_task = terminal_panel.add_terminal(
340+ TerminalKind::Debug {
341+ command,
342+ args,
343+ envs,
344+ cwd,
345+ title,
346+ },
347+ task::RevealStrategy::Always,
348+ window,
349+ cx,
350+ );
351+
352+ cx.spawn(|_, mut cx| async move {
353+ let pid_task = async move {
354+ let terminal = terminal_task.await?;
355+
356+ terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid())
357+ };
358+
359+ pid_task.await
360+ })
361+ })
362+ });
363+
364+ cx.background_spawn(async move {
365+ match terminal_task {
366+ Ok(pid_task) => match pid_task.await {
367+ Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
368+ Ok(None) => {
369+ sender
370+ .send(Err(anyhow!(
371+ "Terminal was spawned but PID was not available"
372+ )))
373+ .await?
374+ }
375+ Err(error) => sender.send(Err(anyhow!(error))).await?,
376+ },
377+ Err(error) => sender.send(Err(anyhow!(error))).await?,
378+ };
379+
380+ Ok(())
381+ })
382+ }
383+
384+ fn handle_pane_event(
385+ &mut self,
386+ _: &Entity<Pane>,
387+ event: &pane::Event,
388+ window: &mut Window,
389+ cx: &mut Context<Self>,
390+ ) {
391+ match event {
392+ pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
393+ pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
394+ pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
395+ pane::Event::AddItem { item } => {
396+ self.workspace
397+ .update(cx, |workspace, cx| {
398+ item.added_to_pane(workspace, self.pane.clone(), window, cx)
399+ })
400+ .ok();
401+ }
402+ pane::Event::RemovedItem { item } => {
403+ if let Some(debug_session) = item.downcast::<DebugSession>() {
404+ debug_session.update(cx, |session, cx| {
405+ session.shutdown(cx);
406+ })
407+ }
408+ }
409+ pane::Event::ActivateItem {
410+ local: _,
411+ focus_changed,
412+ } => {
413+ if *focus_changed {
414+ if let Some(debug_session) = self
415+ .pane
416+ .read(cx)
417+ .active_item()
418+ .and_then(|item| item.downcast::<DebugSession>())
419+ {
420+ if let Some(running) = debug_session
421+ .read_with(cx, |session, _| session.mode().as_running().cloned())
422+ {
423+ running.update(cx, |running, cx| {
424+ running.go_to_selected_stack_frame(window, cx);
425+ });
426+ }
427+ }
428+ }
429+ }
430+
431+ _ => {}
432+ }
433+ }
434+}
435+
436+impl EventEmitter<PanelEvent> for DebugPanel {}
437+impl EventEmitter<DebugPanelEvent> for DebugPanel {}
438+impl EventEmitter<project::Event> for DebugPanel {}
439+
440+impl Focusable for DebugPanel {
441+ fn focus_handle(&self, cx: &App) -> FocusHandle {
442+ self.pane.focus_handle(cx)
443+ }
444+}
445+
446+impl Panel for DebugPanel {
447+ fn pane(&self) -> Option<Entity<Pane>> {
448+ Some(self.pane.clone())
449+ }
450+
451+ fn persistent_name() -> &'static str {
452+ "DebugPanel"
453+ }
454+
455+ fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
456+ DockPosition::Bottom
457+ }
458+
459+ fn position_is_valid(&self, position: DockPosition) -> bool {
460+ position == DockPosition::Bottom
461+ }
462+
463+ fn set_position(
464+ &mut self,
465+ _position: DockPosition,
466+ _window: &mut Window,
467+ _cx: &mut Context<Self>,
468+ ) {
469+ }
470+
471+ fn size(&self, _window: &Window, _cx: &App) -> Pixels {
472+ self.size
473+ }
474+
475+ fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
476+ self.size = size.unwrap();
477+ }
478+
479+ fn remote_id() -> Option<proto::PanelId> {
480+ Some(proto::PanelId::DebugPanel)
481+ }
482+
483+ fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
484+ Some(IconName::Debug)
485+ }
486+
487+ fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
488+ if DebuggerSettings::get_global(cx).button {
489+ Some("Debug Panel")
490+ } else {
491+ None
492+ }
493+ }
494+
495+ fn toggle_action(&self) -> Box<dyn Action> {
496+ Box::new(ToggleFocus)
497+ }
498+
499+ fn activation_priority(&self) -> u32 {
500+ 9
501+ }
502+ fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
503+ if active && self.pane.read(cx).items_len() == 0 {
504+ let Some(project) = self.project.clone().upgrade() else {
505+ return;
506+ };
507+ // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
508+ self.pane.update(cx, |this, cx| {
509+ this.add_item(
510+ Box::new(DebugSession::inert(
511+ project,
512+ self.workspace.clone(),
513+ window,
514+ cx,
515+ )),
516+ false,
517+ false,
518+ None,
519+ window,
520+ cx,
521+ );
522+ });
523+ }
524+ }
525+}
526+
527+impl Render for DebugPanel {
528+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
529+ v_flex()
530+ .key_context("DebugPanel")
531+ .track_focus(&self.focus_handle(cx))
532+ .size_full()
533+ .child(self.pane.clone())
534+ .into_any()
535+ }
536+}
crates/debugger_ui/src/lib.rs+122−0
@@ -0,0 +1,122 @@
1+use dap::debugger_settings::DebuggerSettings;
2+use debugger_panel::{DebugPanel, ToggleFocus};
3+use feature_flags::{Debugger, FeatureFlagViewExt};
4+use gpui::App;
5+use session::DebugSession;
6+use settings::Settings;
7+use workspace::{
8+ Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
9+ ToggleIgnoreBreakpoints, Workspace,
10+};
11+
12+pub mod attach_modal;
13+pub mod debugger_panel;
14+pub mod session;
15+
16+#[cfg(test)]
17+mod tests;
18+
19+pub fn init(cx: &mut App) {
20+ DebuggerSettings::register(cx);
21+ workspace::FollowableViewRegistry::register::<DebugSession>(cx);
22+
23+ cx.observe_new(|_: &mut Workspace, window, cx| {
24+ let Some(window) = window else {
25+ return;
26+ };
27+
28+ cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| {
29+ workspace
30+ .register_action(|workspace, _: &ToggleFocus, window, cx| {
31+ workspace.toggle_panel_focus::<DebugPanel>(window, cx);
32+ })
33+ .register_action(|workspace, _: &Pause, _, cx| {
34+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
35+
36+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
37+ panel
38+ .active_session(cx)
39+ .and_then(|session| session.read(cx).mode().as_running().cloned())
40+ }) {
41+ active_item.update(cx, |item, cx| item.pause_thread(cx))
42+ }
43+ })
44+ .register_action(|workspace, _: &Restart, _, cx| {
45+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
46+
47+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
48+ panel
49+ .active_session(cx)
50+ .and_then(|session| session.read(cx).mode().as_running().cloned())
51+ }) {
52+ active_item.update(cx, |item, cx| item.restart_session(cx))
53+ }
54+ })
55+ .register_action(|workspace, _: &StepInto, _, cx| {
56+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
57+
58+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
59+ panel
60+ .active_session(cx)
61+ .and_then(|session| session.read(cx).mode().as_running().cloned())
62+ }) {
63+ active_item.update(cx, |item, cx| item.step_in(cx))
64+ }
65+ })
66+ .register_action(|workspace, _: &StepOver, _, cx| {
67+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
68+
69+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
70+ panel
71+ .active_session(cx)
72+ .and_then(|session| session.read(cx).mode().as_running().cloned())
73+ }) {
74+ active_item.update(cx, |item, cx| item.step_over(cx))
75+ }
76+ })
77+ .register_action(|workspace, _: &StepBack, _, cx| {
78+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
79+
80+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
81+ panel
82+ .active_session(cx)
83+ .and_then(|session| session.read(cx).mode().as_running().cloned())
84+ }) {
85+ active_item.update(cx, |item, cx| item.step_back(cx))
86+ }
87+ })
88+ .register_action(|workspace, _: &Stop, _, cx| {
89+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
90+
91+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
92+ panel
93+ .active_session(cx)
94+ .and_then(|session| session.read(cx).mode().as_running().cloned())
95+ }) {
96+ active_item.update(cx, |item, cx| item.stop_thread(cx))
97+ }
98+ })
99+ .register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
100+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
101+
102+ if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
103+ panel
104+ .active_session(cx)
105+ .and_then(|session| session.read(cx).mode().as_running().cloned())
106+ }) {
107+ active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
108+ }
109+ })
110+ .register_action(
111+ |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
112+ workspace.project().update(cx, |project, cx| {
113+ project.dap_store().update(cx, |store, cx| {
114+ store.shutdown_sessions(cx).detach();
115+ })
116+ })
117+ },
118+ );
119+ })
120+ })
121+ .detach();
122+}
crates/debugger_ui/src/session.rs+313−0
@@ -0,0 +1,313 @@
1+mod failed;
2+mod inert;
3+pub mod running;
4+mod starting;
5+
6+use std::time::Duration;
7+
8+use dap::client::SessionId;
9+use failed::FailedState;
10+use gpui::{
11+ percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
12+ Focusable, Subscription, Task, Transformation, WeakEntity,
13+};
14+use inert::{InertEvent, InertState};
15+use project::debugger::{dap_store::DapStore, session::Session};
16+use project::worktree_store::WorktreeStore;
17+use project::Project;
18+use rpc::proto::{self, PeerId};
19+use running::RunningState;
20+use starting::{StartingEvent, StartingState};
21+use ui::prelude::*;
22+use workspace::{
23+ item::{self, Item},
24+ FollowableItem, ViewId, Workspace,
25+};
26+
27+pub(crate) enum DebugSessionState {
28+ Inert(Entity<InertState>),
29+ Starting(Entity<StartingState>),
30+ Failed(Entity<FailedState>),
31+ Running(Entity<running::RunningState>),
32+}
33+
34+impl DebugSessionState {
35+ pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> {
36+ match &self {
37+ DebugSessionState::Running(entity) => Some(entity),
38+ _ => None,
39+ }
40+ }
41+}
42+
43+pub struct DebugSession {
44+ remote_id: Option<workspace::ViewId>,
45+ mode: DebugSessionState,
46+ dap_store: WeakEntity<DapStore>,
47+ worktree_store: WeakEntity<WorktreeStore>,
48+ workspace: WeakEntity<Workspace>,
49+ _subscriptions: [Subscription; 1],
50+}
51+
52+#[derive(Debug)]
53+pub enum DebugPanelItemEvent {
54+ Close,
55+ Stopped { go_to_stack_frame: bool },
56+}
57+
58+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
59+pub enum ThreadItem {
60+ Console,
61+ LoadedSource,
62+ Modules,
63+ Variables,
64+}
65+
66+impl DebugSession {
67+ pub(super) fn inert(
68+ project: Entity<Project>,
69+ workspace: WeakEntity<Workspace>,
70+ window: &mut Window,
71+ cx: &mut App,
72+ ) -> Entity<Self> {
73+ let default_cwd = project
74+ .read(cx)
75+ .worktrees(cx)
76+ .next()
77+ .and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
78+ .unwrap_or_default();
79+
80+ let inert = cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, window, cx));
81+
82+ let project = project.read(cx);
83+ let dap_store = project.dap_store().downgrade();
84+ let worktree_store = project.worktree_store().downgrade();
85+ cx.new(|cx| {
86+ let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
87+ Self {
88+ remote_id: None,
89+ mode: DebugSessionState::Inert(inert),
90+ dap_store,
91+ worktree_store,
92+ workspace,
93+ _subscriptions,
94+ }
95+ })
96+ }
97+
98+ pub(crate) fn running(
99+ project: Entity<Project>,
100+ workspace: WeakEntity<Workspace>,
101+ session: Entity<Session>,
102+ window: &mut Window,
103+ cx: &mut App,
104+ ) -> Entity<Self> {
105+ let mode = cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx));
106+
107+ cx.new(|cx| Self {
108+ _subscriptions: [cx.subscribe(&mode, |_, _, _, cx| {
109+ cx.notify();
110+ })],
111+ remote_id: None,
112+ mode: DebugSessionState::Running(mode),
113+ dap_store: project.read(cx).dap_store().downgrade(),
114+ worktree_store: project.read(cx).worktree_store().downgrade(),
115+ workspace,
116+ })
117+ }
118+
119+ pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> {
120+ match &self.mode {
121+ DebugSessionState::Inert(_) => None,
122+ DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id),
123+ DebugSessionState::Failed(_) => None,
124+ DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
125+ }
126+ }
127+
128+ pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
129+ match &self.mode {
130+ DebugSessionState::Inert(_) => {}
131+ DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit)
132+ DebugSessionState::Failed(_) => {}
133+ DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
134+ }
135+ }
136+
137+ pub(crate) fn mode(&self) -> &DebugSessionState {
138+ &self.mode
139+ }
140+
141+ fn on_inert_event(
142+ &mut self,
143+ _: &Entity<InertState>,
144+ event: &InertEvent,
145+ window: &mut Window,
146+ cx: &mut Context<'_, Self>,
147+ ) {
148+ let dap_store = self.dap_store.clone();
149+ let InertEvent::Spawned { config } = event;
150+ let config = config.clone();
151+ let worktree = self
152+ .worktree_store
153+ .update(cx, |this, _| this.worktrees().next())
154+ .ok()
155+ .flatten()
156+ .expect("worktree-less project");
157+ let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
158+ store.new_session(config, &worktree, None, cx)
159+ }) else {
160+ return;
161+ };
162+ let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx));
163+
164+ self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
165+ self.mode = DebugSessionState::Starting(starting);
166+ }
167+
168+ fn on_starting_event(
169+ &mut self,
170+ _: &Entity<StartingState>,
171+ event: &StartingEvent,
172+ window: &mut Window,
173+ cx: &mut Context<'_, Self>,
174+ ) {
175+ if let StartingEvent::Finished(session) = event {
176+ let mode =
177+ cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
178+ self.mode = DebugSessionState::Running(mode);
179+ } else if let StartingEvent::Failed = event {
180+ self.mode = DebugSessionState::Failed(cx.new(FailedState::new));
181+ };
182+ cx.notify();
183+ }
184+}
185+impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
186+
187+impl Focusable for DebugSession {
188+ fn focus_handle(&self, cx: &App) -> FocusHandle {
189+ match &self.mode {
190+ DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
191+ DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
192+ DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx),
193+ DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
194+ }
195+ }
196+}
197+
198+impl Item for DebugSession {
199+ type Event = DebugPanelItemEvent;
200+ fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
201+ let (label, color) = match &self.mode {
202+ DebugSessionState::Inert(_) => ("New Session", Color::Default),
203+ DebugSessionState::Starting(_) => ("Starting", Color::Default),
204+ DebugSessionState::Failed(_) => ("Failed", Color::Error),
205+ DebugSessionState::Running(state) => (
206+ state
207+ .read_with(cx, |state, cx| state.thread_status(cx))
208+ .map(|status| status.label())
209+ .unwrap_or("Running"),
210+ Color::Default,
211+ ),
212+ };
213+
214+ let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
215+
216+ h_flex()
217+ .gap_1()
218+ .children(is_starting.then(|| {
219+ Icon::new(IconName::ArrowCircle).with_animation(
220+ "starting-debug-session",
221+ Animation::new(Duration::from_secs(2)).repeat(),
222+ |this, delta| this.transform(Transformation::rotate(percentage(delta))),
223+ )
224+ }))
225+ .child(Label::new(label).color(color))
226+ .into_any_element()
227+ }
228+}
229+
230+impl FollowableItem for DebugSession {
231+ fn remote_id(&self) -> Option<workspace::ViewId> {
232+ self.remote_id
233+ }
234+
235+ fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option<proto::view::Variant> {
236+ None
237+ }
238+
239+ fn from_state_proto(
240+ _workspace: Entity<Workspace>,
241+ _remote_id: ViewId,
242+ _state: &mut Option<proto::view::Variant>,
243+ _window: &mut Window,
244+ _cx: &mut App,
245+ ) -> Option<gpui::Task<gpui::Result<Entity<Self>>>> {
246+ None
247+ }
248+
249+ fn add_event_to_update_proto(
250+ &self,
251+ _event: &Self::Event,
252+ _update: &mut Option<proto::update_view::Variant>,
253+ _window: &Window,
254+ _cx: &App,
255+ ) -> bool {
256+ // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default()));
257+
258+ true
259+ }
260+
261+ fn apply_update_proto(
262+ &mut self,
263+ _project: &Entity<project::Project>,
264+ _message: proto::update_view::Variant,
265+ _window: &mut Window,
266+ _cx: &mut Context<Self>,
267+ ) -> gpui::Task<gpui::Result<()>> {
268+ Task::ready(Ok(()))
269+ }
270+
271+ fn set_leader_peer_id(
272+ &mut self,
273+ _leader_peer_id: Option<PeerId>,
274+ _window: &mut Window,
275+ _cx: &mut Context<Self>,
276+ ) {
277+ }
278+
279+ fn to_follow_event(_event: &Self::Event) -> Option<workspace::item::FollowEvent> {
280+ None
281+ }
282+
283+ fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<workspace::item::Dedup> {
284+ if existing.session_id(cx) == self.session_id(cx) {
285+ Some(item::Dedup::KeepExisting)
286+ } else {
287+ None
288+ }
289+ }
290+
291+ fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
292+ true
293+ }
294+}
295+
296+impl Render for DebugSession {
297+ fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
298+ match &self.mode {
299+ DebugSessionState::Inert(inert_state) => {
300+ inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
301+ }
302+ DebugSessionState::Starting(starting_state) => {
303+ starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
304+ }
305+ DebugSessionState::Failed(failed_state) => {
306+ failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
307+ }
308+ DebugSessionState::Running(running_state) => {
309+ running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
310+ }
311+ }
312+ }
313+}
crates/debugger_ui/src/session/failed.rs+30−0
@@ -0,0 +1,30 @@
1+use gpui::{FocusHandle, Focusable};
2+use ui::{
3+ h_flex, Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window,
4+};
5+
6+pub(crate) struct FailedState {
7+ focus_handle: FocusHandle,
8+}
9+impl FailedState {
10+ pub(super) fn new(cx: &mut Context<Self>) -> Self {
11+ Self {
12+ focus_handle: cx.focus_handle(),
13+ }
14+ }
15+}
16+
17+impl Focusable for FailedState {
18+ fn focus_handle(&self, _: &ui::App) -> FocusHandle {
19+ self.focus_handle.clone()
20+ }
21+}
22+impl Render for FailedState {
23+ fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24+ h_flex()
25+ .size_full()
26+ .items_center()
27+ .justify_center()
28+ .child(Label::new("Failed to spawn debugging session").color(Color::Error))
29+ }
30+}
crates/debugger_ui/src/session/inert.rs+219−0
@@ -0,0 +1,219 @@
1+use std::path::PathBuf;
2+
3+use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
4+use editor::{Editor, EditorElement, EditorStyle};
5+use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
6+use settings::Settings as _;
7+use task::TCPHost;
8+use theme::ThemeSettings;
9+use ui::{
10+ h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable,
11+ Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement,
12+ ParentElement, Render, SharedString, Styled, Window,
13+};
14+use workspace::Workspace;
15+
16+use crate::attach_modal::AttachModal;
17+
18+pub(crate) struct InertState {
19+ focus_handle: FocusHandle,
20+ selected_debugger: Option<SharedString>,
21+ program_editor: Entity<Editor>,
22+ cwd_editor: Entity<Editor>,
23+ workspace: WeakEntity<Workspace>,
24+}
25+
26+impl InertState {
27+ pub(super) fn new(
28+ workspace: WeakEntity<Workspace>,
29+ default_cwd: &str,
30+ window: &mut Window,
31+ cx: &mut Context<Self>,
32+ ) -> Self {
33+ let program_editor = cx.new(|cx| {
34+ let mut editor = Editor::single_line(window, cx);
35+ editor.set_placeholder_text("Program path", cx);
36+ editor
37+ });
38+ let cwd_editor = cx.new(|cx| {
39+ let mut editor = Editor::single_line(window, cx);
40+ editor.insert(default_cwd, window, cx);
41+ editor.set_placeholder_text("Working directory", cx);
42+ editor
43+ });
44+ Self {
45+ workspace,
46+ cwd_editor,
47+ program_editor,
48+ selected_debugger: None,
49+ focus_handle: cx.focus_handle(),
50+ }
51+ }
52+}
53+impl Focusable for InertState {
54+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
55+ self.focus_handle.clone()
56+ }
57+}
58+
59+pub(crate) enum InertEvent {
60+ Spawned { config: DebugAdapterConfig },
61+}
62+
63+impl EventEmitter<InertEvent> for InertState {}
64+
65+static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
66+
67+impl Render for InertState {
68+ fn render(
69+ &mut self,
70+ window: &mut ui::Window,
71+ cx: &mut ui::Context<'_, Self>,
72+ ) -> impl ui::IntoElement {
73+ let weak = cx.weak_entity();
74+ let disable_buttons = self.selected_debugger.is_none();
75+ v_flex()
76+ .track_focus(&self.focus_handle)
77+ .size_full()
78+ .gap_1()
79+ .p_2()
80+
81+ .child(
82+ v_flex().gap_1()
83+ .child(
84+ h_flex()
85+ .w_full()
86+ .gap_2()
87+
88+ .child(Self::render_editor(&self.program_editor, cx))
89+ .child(
90+ h_flex().child(DropdownMenu::new(
91+ "dap-adapter-picker",
92+ self.selected_debugger
93+ .as_ref()
94+ .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
95+ .clone(),
96+ ContextMenu::build(window, cx, move |this, _, _| {
97+ let setter_for_name = |name: &'static str| {
98+ let weak = weak.clone();
99+ move |_: &mut Window, cx: &mut App| {
100+ let name = name;
101+ (&weak)
102+ .update(cx, move |this, _| {
103+ this.selected_debugger = Some(name.into());
104+ })
105+ .ok();
106+ }
107+ };
108+ this.entry("GDB", None, setter_for_name("GDB"))
109+ .entry("Delve", None, setter_for_name("Delve"))
110+ .entry("LLDB", None, setter_for_name("LLDB"))
111+ .entry("PHP", None, setter_for_name("PHP"))
112+ .entry("JavaScript", None, setter_for_name("JavaScript"))
113+ .entry("Debugpy", None, setter_for_name("Debugpy"))
114+ }),
115+ )),
116+ )
117+ )
118+ .child(
119+ h_flex().gap_2().child(
120+ Self::render_editor(&self.cwd_editor, cx),
121+ ).child(h_flex()
122+ .gap_4()
123+ .pl_2()
124+ .child(
125+ Button::new("launch-dap", "Launch")
126+ .style(ButtonStyle::Filled)
127+ .disabled(disable_buttons)
128+ .on_click(cx.listener(|this, _, _, cx| {
129+ let program = this.program_editor.read(cx).text(cx);
130+ let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
131+ let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project")));
132+ cx.emit(InertEvent::Spawned {
133+ config: DebugAdapterConfig {
134+ label: "hard coded".into(),
135+ kind,
136+ request: DebugRequestType::Launch,
137+ program: Some(program),
138+ cwd: Some(cwd),
139+ initialize_args: None,
140+ supports_attach: false,
141+ },
142+ });
143+ })),
144+ )
145+ .child(Button::new("attach-dap", "Attach")
146+ .style(ButtonStyle::Filled)
147+ .disabled(disable_buttons)
148+ .on_click(cx.listener(|this, _, window, cx| this.attach(window, cx)))
149+ ))
150+ )
151+ )
152+ }
153+}
154+
155+fn kind_for_label(label: &str) -> DebugAdapterKind {
156+ match label {
157+ "LLDB" => DebugAdapterKind::Lldb,
158+ "Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
159+ "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
160+ "PHP" => DebugAdapterKind::Php(TCPHost::default()),
161+ "Delve" => DebugAdapterKind::Go(TCPHost::default()),
162+ _ => {
163+ unimplemented!()
164+ } // Maybe we should set a toast notification here
165+ }
166+}
167+impl InertState {
168+ fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
169+ let settings = ThemeSettings::get_global(cx);
170+ let text_style = TextStyle {
171+ color: cx.theme().colors().text,
172+ font_family: settings.buffer_font.family.clone(),
173+ font_features: settings.buffer_font.features.clone(),
174+ font_size: settings.buffer_font_size(cx).into(),
175+ font_weight: settings.buffer_font.weight,
176+ line_height: relative(settings.buffer_line_height.value()),
177+ ..Default::default()
178+ };
179+
180+ EditorElement::new(
181+ editor,
182+ EditorStyle {
183+ background: cx.theme().colors().editor_background,
184+ local_player: cx.theme().players().local(),
185+ text: text_style,
186+ ..Default::default()
187+ },
188+ )
189+ }
190+
191+ fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
192+ let process_id = self.program_editor.read(cx).text(cx).parse::<u32>().ok();
193+ let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
194+ let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
195+ unimplemented!("Automatic selection of a debugger based on users project")
196+ }));
197+
198+ let config = DebugAdapterConfig {
199+ label: "hard coded attach".into(),
200+ kind,
201+ request: DebugRequestType::Attach(task::AttachConfig { process_id }),
202+ program: None,
203+ cwd: Some(cwd),
204+ initialize_args: None,
205+ supports_attach: true,
206+ };
207+
208+ if process_id.is_some() {
209+ cx.emit(InertEvent::Spawned { config });
210+ } else {
211+ let _ = self.workspace.update(cx, |workspace, cx| {
212+ let project = workspace.project().clone();
213+ workspace.toggle_modal(window, cx, |window, cx| {
214+ AttachModal::new(project, config, window, cx)
215+ });
216+ });
217+ }
218+ }
219+}
crates/debugger_ui/src/session/running.rs+686−0
@@ -0,0 +1,686 @@
1+mod console;
2+mod loaded_source_list;
3+mod module_list;
4+pub mod stack_frame_list;
5+pub mod variable_list;
6+
7+use super::{DebugPanelItemEvent, ThreadItem};
8+use console::Console;
9+use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread};
10+use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
11+use loaded_source_list::LoadedSourceList;
12+use module_list::ModuleList;
13+use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
14+use rpc::proto::ViewId;
15+use settings::Settings;
16+use stack_frame_list::StackFrameList;
17+use ui::{
18+ div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
19+ ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
20+ Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
21+ StatefulInteractiveElement, Styled, Tooltip, Window,
22+};
23+use util::ResultExt;
24+use variable_list::VariableList;
25+use workspace::Workspace;
26+
27+pub struct RunningState {
28+ session: Entity<Session>,
29+ thread_id: Option<ThreadId>,
30+ console: Entity<console::Console>,
31+ focus_handle: FocusHandle,
32+ _remote_id: Option<ViewId>,
33+ show_console_indicator: bool,
34+ module_list: Entity<module_list::ModuleList>,
35+ active_thread_item: ThreadItem,
36+ workspace: WeakEntity<Workspace>,
37+ session_id: SessionId,
38+ variable_list: Entity<variable_list::VariableList>,
39+ _subscriptions: Vec<Subscription>,
40+ stack_frame_list: Entity<stack_frame_list::StackFrameList>,
41+ loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
42+}
43+
44+impl Render for RunningState {
45+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
46+ let threads = self.session.update(cx, |this, cx| this.threads(cx));
47+ self.select_current_thread(&threads, cx);
48+
49+ let thread_status = self
50+ .thread_id
51+ .map(|thread_id| self.session.read(cx).thread_status(thread_id))
52+ .unwrap_or(ThreadStatus::Exited);
53+
54+ let selected_thread_name = threads
55+ .iter()
56+ .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
57+ .map(|(thread, _)| thread.name.clone())
58+ .unwrap_or("Threads".to_owned());
59+
60+ self.variable_list.update(cx, |this, cx| {
61+ this.disabled(thread_status != ThreadStatus::Stopped, cx);
62+ });
63+
64+ let is_terminated = self.session.read(cx).is_terminated();
65+ let active_thread_item = &self.active_thread_item;
66+
67+ let has_no_threads = threads.is_empty();
68+ let capabilities = self.capabilities(cx);
69+ let state = cx.entity();
70+ h_flex()
71+ .when(is_terminated, |this| this.bg(gpui::red()))
72+ .key_context("DebugPanelItem")
73+ .track_focus(&self.focus_handle(cx))
74+ .size_full()
75+ .items_start()
76+ .child(
77+ v_flex()
78+ .size_full()
79+ .items_start()
80+ .child(
81+ h_flex()
82+ .w_full()
83+ .border_b_1()
84+ .border_color(cx.theme().colors().border_variant)
85+ .justify_between()
86+ .child(
87+ h_flex()
88+ .p_1()
89+ .w_full()
90+ .gap_2()
91+ .map(|this| {
92+ if thread_status == ThreadStatus::Running {
93+ this.child(
94+ IconButton::new(
95+ "debug-pause",
96+ IconName::DebugPause,
97+ )
98+ .icon_size(IconSize::Small)
99+ .on_click(cx.listener(|this, _, _window, cx| {
100+ this.pause_thread(cx);
101+ }))
102+ .tooltip(move |window, cx| {
103+ Tooltip::text("Pause program")(window, cx)
104+ }),
105+ )
106+ } else {
107+ this.child(
108+ IconButton::new(
109+ "debug-continue",
110+ IconName::DebugContinue,
111+ )
112+ .icon_size(IconSize::Small)
113+ .on_click(cx.listener(|this, _, _window, cx| {
114+ this.continue_thread(cx)
115+ }))
116+ .disabled(thread_status != ThreadStatus::Stopped)
117+ .tooltip(move |window, cx| {
118+ Tooltip::text("Continue program")(window, cx)
119+ }),
120+ )
121+ }
122+ })
123+ .when(
124+ capabilities.supports_step_back.unwrap_or(false),
125+ |this| {
126+ this.child(
127+ IconButton::new(
128+ "debug-step-back",
129+ IconName::DebugStepBack,
130+ )
131+ .icon_size(IconSize::Small)
132+ .on_click(cx.listener(|this, _, _window, cx| {
133+ this.step_back(cx);
134+ }))
135+ .disabled(thread_status != ThreadStatus::Stopped)
136+ .tooltip(move |window, cx| {
137+ Tooltip::text("Step back")(window, cx)
138+ }),
139+ )
140+ },
141+ )
142+ .child(
143+ IconButton::new("debug-step-over", IconName::DebugStepOver)
144+ .icon_size(IconSize::Small)
145+ .on_click(cx.listener(|this, _, _window, cx| {
146+ this.step_over(cx);
147+ }))
148+ .disabled(thread_status != ThreadStatus::Stopped)
149+ .tooltip(move |window, cx| {
150+ Tooltip::text("Step over")(window, cx)
151+ }),
152+ )
153+ .child(
154+ IconButton::new("debug-step-in", IconName::DebugStepInto)
155+ .icon_size(IconSize::Small)
156+ .on_click(cx.listener(|this, _, _window, cx| {
157+ this.step_in(cx);
158+ }))
159+ .disabled(thread_status != ThreadStatus::Stopped)
160+ .tooltip(move |window, cx| {
161+ Tooltip::text("Step in")(window, cx)
162+ }),
163+ )
164+ .child(
165+ IconButton::new("debug-step-out", IconName::DebugStepOut)
166+ .icon_size(IconSize::Small)
167+ .on_click(cx.listener(|this, _, _window, cx| {
168+ this.step_out(cx);
169+ }))
170+ .disabled(thread_status != ThreadStatus::Stopped)
171+ .tooltip(move |window, cx| {
172+ Tooltip::text("Step out")(window, cx)
173+ }),
174+ )
175+ .child(
176+ IconButton::new("debug-restart", IconName::DebugRestart)
177+ .icon_size(IconSize::Small)
178+ .on_click(cx.listener(|this, _, _window, cx| {
179+ this.restart_session(cx);
180+ }))
181+ .disabled(
182+ !capabilities
183+ .supports_restart_request
184+ .unwrap_or_default(),
185+ )
186+ .tooltip(move |window, cx| {
187+ Tooltip::text("Restart")(window, cx)
188+ }),
189+ )
190+ .child(
191+ IconButton::new("debug-stop", IconName::DebugStop)
192+ .icon_size(IconSize::Small)
193+ .on_click(cx.listener(|this, _, _window, cx| {
194+ this.stop_thread(cx);
195+ }))
196+ .disabled(
197+ thread_status != ThreadStatus::Stopped
198+ && thread_status != ThreadStatus::Running,
199+ )
200+ .tooltip({
201+ let label = if capabilities
202+ .supports_terminate_threads_request
203+ .unwrap_or_default()
204+ {
205+ "Terminate Thread"
206+ } else {
207+ "Terminate all Threads"
208+ };
209+ move |window, cx| Tooltip::text(label)(window, cx)
210+ }),
211+ )
212+ .child(
213+ IconButton::new(
214+ "debug-disconnect",
215+ IconName::DebugDisconnect,
216+ )
217+ .icon_size(IconSize::Small)
218+ .on_click(cx.listener(|this, _, _window, cx| {
219+ this.disconnect_client(cx);
220+ }))
221+ .disabled(
222+ thread_status == ThreadStatus::Exited
223+ || thread_status == ThreadStatus::Ended,
224+ )
225+ .tooltip(
226+ move |window, cx| {
227+ Tooltip::text("Disconnect")(window, cx)
228+ },
229+ ),
230+ )
231+ .child(
232+ IconButton::new(
233+ "debug-ignore-breakpoints",
234+ if self.session.read(cx).breakpoints_enabled() {
235+ IconName::DebugBreakpoint
236+ } else {
237+ IconName::DebugIgnoreBreakpoints
238+ },
239+ )
240+ .icon_size(IconSize::Small)
241+ .on_click(cx.listener(|this, _, _window, cx| {
242+ this.toggle_ignore_breakpoints(cx);
243+ }))
244+ .disabled(
245+ thread_status == ThreadStatus::Exited
246+ || thread_status == ThreadStatus::Ended,
247+ )
248+ .tooltip(
249+ move |window, cx| {
250+ Tooltip::text("Ignore breakpoints")(window, cx)
251+ },
252+ ),
253+ ),
254+ )
255+ //.child(h_flex())
256+ .child(
257+ h_flex().p_1().mx_2().w_3_4().justify_end().child(
258+ DropdownMenu::new(
259+ ("thread-list", self.session_id.0),
260+ selected_thread_name,
261+ ContextMenu::build(window, cx, move |mut this, _, _| {
262+ for (thread, _) in threads {
263+ let state = state.clone();
264+ let thread_id = thread.id;
265+ this =
266+ this.entry(thread.name, None, move |_, cx| {
267+ state.update(cx, |state, cx| {
268+ state.select_thread(
269+ ThreadId(thread_id),
270+ cx,
271+ );
272+ });
273+ });
274+ }
275+ this
276+ }),
277+ )
278+ .disabled(
279+ has_no_threads || thread_status != ThreadStatus::Stopped,
280+ ),
281+ ),
282+ ),
283+ )
284+ .child(
285+ h_flex()
286+ .size_full()
287+ .items_start()
288+ .p_1()
289+ .gap_4()
290+ .child(self.stack_frame_list.clone()),
291+ ),
292+ )
293+ .child(
294+ v_flex()
295+ .border_l_1()
296+ .border_color(cx.theme().colors().border_variant)
297+ .size_full()
298+ .items_start()
299+ .child(
300+ h_flex()
301+ .border_b_1()
302+ .w_full()
303+ .border_color(cx.theme().colors().border_variant)
304+ .child(self.render_entry_button(
305+ &SharedString::from("Variables"),
306+ ThreadItem::Variables,
307+ cx,
308+ ))
309+ .when(
310+ capabilities.supports_modules_request.unwrap_or_default(),
311+ |this| {
312+ this.child(self.render_entry_button(
313+ &SharedString::from("Modules"),
314+ ThreadItem::Modules,
315+ cx,
316+ ))
317+ },
318+ )
319+ .when(
320+ capabilities
321+ .supports_loaded_sources_request
322+ .unwrap_or_default(),
323+ |this| {
324+ this.child(self.render_entry_button(
325+ &SharedString::from("Loaded Sources"),
326+ ThreadItem::LoadedSource,
327+ cx,
328+ ))
329+ },
330+ )
331+ .child(self.render_entry_button(
332+ &SharedString::from("Console"),
333+ ThreadItem::Console,
334+ cx,
335+ )),
336+ )
337+ .when(*active_thread_item == ThreadItem::Variables, |this| {
338+ this.child(self.variable_list.clone())
339+ })
340+ .when(*active_thread_item == ThreadItem::Modules, |this| {
341+ this.size_full().child(self.module_list.clone())
342+ })
343+ .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
344+ this.size_full().child(self.loaded_source_list.clone())
345+ })
346+ .when(*active_thread_item == ThreadItem::Console, |this| {
347+ this.child(self.console.clone())
348+ }),
349+ )
350+ }
351+}
352+
353+impl RunningState {
354+ pub fn new(
355+ session: Entity<Session>,
356+ workspace: WeakEntity<Workspace>,
357+ window: &mut Window,
358+ cx: &mut Context<Self>,
359+ ) -> Self {
360+ let focus_handle = cx.focus_handle();
361+ let session_id = session.read(cx).session_id();
362+ let weak_state = cx.weak_entity();
363+ let stack_frame_list = cx.new(|cx| {
364+ StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
365+ });
366+
367+ let variable_list =
368+ cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
369+
370+ let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
371+
372+ let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
373+
374+ let console = cx.new(|cx| {
375+ Console::new(
376+ session.clone(),
377+ stack_frame_list.clone(),
378+ variable_list.clone(),
379+ window,
380+ cx,
381+ )
382+ });
383+
384+ let _subscriptions = vec![
385+ cx.observe(&module_list, |_, _, cx| cx.notify()),
386+ cx.subscribe_in(&session, window, |this, _, event, window, cx| {
387+ match event {
388+ SessionEvent::Stopped(thread_id) => {
389+ this.workspace
390+ .update(cx, |workspace, cx| {
391+ workspace.open_panel::<crate::DebugPanel>(window, cx);
392+ })
393+ .log_err();
394+
395+ if let Some(thread_id) = thread_id {
396+ this.select_thread(*thread_id, cx);
397+ }
398+ }
399+ SessionEvent::Threads => {
400+ let threads = this.session.update(cx, |this, cx| this.threads(cx));
401+ this.select_current_thread(&threads, cx);
402+ }
403+ _ => {}
404+ }
405+ cx.notify()
406+ }),
407+ ];
408+
409+ Self {
410+ session,
411+ console,
412+ workspace,
413+ module_list,
414+ focus_handle,
415+ variable_list,
416+ _subscriptions,
417+ thread_id: None,
418+ _remote_id: None,
419+ stack_frame_list,
420+ loaded_source_list,
421+ session_id,
422+ show_console_indicator: false,
423+ active_thread_item: ThreadItem::Variables,
424+ }
425+ }
426+
427+ pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
428+ if self.thread_id.is_some() {
429+ self.stack_frame_list
430+ .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
431+ }
432+ }
433+
434+ pub fn session(&self) -> &Entity<Session> {
435+ &self.session
436+ }
437+
438+ pub fn session_id(&self) -> SessionId {
439+ self.session_id
440+ }
441+
442+ #[cfg(any(test, feature = "test-support"))]
443+ pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
444+ self.active_thread_item = thread_item;
445+ cx.notify()
446+ }
447+
448+ #[cfg(any(test, feature = "test-support"))]
449+ pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
450+ &self.stack_frame_list
451+ }
452+
453+ #[cfg(any(test, feature = "test-support"))]
454+ pub fn console(&self) -> &Entity<Console> {
455+ &self.console
456+ }
457+
458+ #[cfg(any(test, feature = "test-support"))]
459+ pub fn module_list(&self) -> &Entity<ModuleList> {
460+ &self.module_list
461+ }
462+
463+ #[cfg(any(test, feature = "test-support"))]
464+ pub fn variable_list(&self) -> &Entity<VariableList> {
465+ &self.variable_list
466+ }
467+
468+ #[cfg(any(test, feature = "test-support"))]
469+ pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
470+ self.session.read(cx).ignore_breakpoints()
471+ }
472+
473+ pub fn capabilities(&self, cx: &App) -> Capabilities {
474+ self.session().read(cx).capabilities().clone()
475+ }
476+
477+ pub fn select_current_thread(
478+ &mut self,
479+ threads: &Vec<(Thread, ThreadStatus)>,
480+ cx: &mut Context<Self>,
481+ ) {
482+ let selected_thread = self
483+ .thread_id
484+ .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
485+ .or_else(|| threads.first());
486+
487+ let Some((selected_thread, _)) = selected_thread else {
488+ return;
489+ };
490+
491+ if Some(ThreadId(selected_thread.id)) != self.thread_id {
492+ self.select_thread(ThreadId(selected_thread.id), cx);
493+ }
494+ }
495+
496+ #[cfg(any(test, feature = "test-support"))]
497+ pub fn selected_thread_id(&self) -> Option<ThreadId> {
498+ self.thread_id
499+ }
500+
501+ pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
502+ self.thread_id
503+ .map(|id| self.session().read(cx).thread_status(id))
504+ }
505+
506+ fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
507+ if self.thread_id.is_some_and(|id| id == thread_id) {
508+ return;
509+ }
510+
511+ self.thread_id = Some(thread_id);
512+
513+ self.stack_frame_list
514+ .update(cx, |list, cx| list.refresh(cx));
515+ cx.notify();
516+ }
517+
518+ fn render_entry_button(
519+ &self,
520+ label: &SharedString,
521+ thread_item: ThreadItem,
522+ cx: &mut Context<Self>,
523+ ) -> AnyElement {
524+ let has_indicator =
525+ matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
526+
527+ div()
528+ .id(label.clone())
529+ .px_2()
530+ .py_1()
531+ .cursor_pointer()
532+ .border_b_2()
533+ .when(self.active_thread_item == thread_item, |this| {
534+ this.border_color(cx.theme().colors().border)
535+ })
536+ .child(
537+ h_flex()
538+ .child(Button::new(label.clone(), label.clone()))
539+ .when(has_indicator, |this| this.child(Indicator::dot())),
540+ )
541+ .on_click(cx.listener(move |this, _, _window, cx| {
542+ this.active_thread_item = thread_item;
543+
544+ if matches!(this.active_thread_item, ThreadItem::Console) {
545+ this.show_console_indicator = false;
546+ }
547+
548+ cx.notify();
549+ }))
550+ .into_any_element()
551+ }
552+
553+ pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
554+ let Some(thread_id) = self.thread_id else {
555+ return;
556+ };
557+
558+ self.session().update(cx, |state, cx| {
559+ state.continue_thread(thread_id, cx);
560+ });
561+ }
562+
563+ pub fn step_over(&mut self, cx: &mut Context<Self>) {
564+ let Some(thread_id) = self.thread_id else {
565+ return;
566+ };
567+
568+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
569+
570+ self.session().update(cx, |state, cx| {
571+ state.step_over(thread_id, granularity, cx);
572+ });
573+ }
574+
575+ pub fn step_in(&mut self, cx: &mut Context<Self>) {
576+ let Some(thread_id) = self.thread_id else {
577+ return;
578+ };
579+
580+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
581+
582+ self.session().update(cx, |state, cx| {
583+ state.step_in(thread_id, granularity, cx);
584+ });
585+ }
586+
587+ pub fn step_out(&mut self, cx: &mut Context<Self>) {
588+ let Some(thread_id) = self.thread_id else {
589+ return;
590+ };
591+
592+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
593+
594+ self.session().update(cx, |state, cx| {
595+ state.step_out(thread_id, granularity, cx);
596+ });
597+ }
598+
599+ pub fn step_back(&mut self, cx: &mut Context<Self>) {
600+ let Some(thread_id) = self.thread_id else {
601+ return;
602+ };
603+
604+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
605+
606+ self.session().update(cx, |state, cx| {
607+ state.step_back(thread_id, granularity, cx);
608+ });
609+ }
610+
611+ pub fn restart_session(&self, cx: &mut Context<Self>) {
612+ self.session().update(cx, |state, cx| {
613+ state.restart(None, cx);
614+ });
615+ }
616+
617+ pub fn pause_thread(&self, cx: &mut Context<Self>) {
618+ let Some(thread_id) = self.thread_id else {
619+ return;
620+ };
621+
622+ self.session().update(cx, |state, cx| {
623+ state.pause_thread(thread_id, cx);
624+ });
625+ }
626+
627+ pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
628+ self.workspace
629+ .update(cx, |workspace, cx| {
630+ workspace
631+ .project()
632+ .read(cx)
633+ .breakpoint_store()
634+ .update(cx, |store, cx| {
635+ store.remove_active_position(Some(self.session_id), cx)
636+ })
637+ })
638+ .log_err();
639+
640+ self.session.update(cx, |session, cx| {
641+ session.shutdown(cx).detach();
642+ })
643+ }
644+
645+ pub fn stop_thread(&self, cx: &mut Context<Self>) {
646+ let Some(thread_id) = self.thread_id else {
647+ return;
648+ };
649+
650+ self.workspace
651+ .update(cx, |workspace, cx| {
652+ workspace
653+ .project()
654+ .read(cx)
655+ .breakpoint_store()
656+ .update(cx, |store, cx| {
657+ store.remove_active_position(Some(self.session_id), cx)
658+ })
659+ })
660+ .log_err();
661+
662+ self.session().update(cx, |state, cx| {
663+ state.terminate_threads(Some(vec![thread_id; 1]), cx);
664+ });
665+ }
666+
667+ pub fn disconnect_client(&self, cx: &mut Context<Self>) {
668+ self.session().update(cx, |state, cx| {
669+ state.disconnect_client(cx);
670+ });
671+ }
672+
673+ pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
674+ self.session.update(cx, |session, cx| {
675+ session.toggle_ignore_breakpoints(cx).detach();
676+ });
677+ }
678+}
679+
680+impl EventEmitter<DebugPanelItemEvent> for RunningState {}
681+
682+impl Focusable for RunningState {
683+ fn focus_handle(&self, _: &App) -> FocusHandle {
684+ self.focus_handle.clone()
685+ }
686+}
crates/debugger_ui/src/session/running/console.rs+419−0
@@ -0,0 +1,419 @@
1+use super::{
2+ stack_frame_list::{StackFrameList, StackFrameListEvent},
3+ variable_list::VariableList,
4+};
5+use anyhow::Result;
6+use collections::HashMap;
7+use dap::OutputEvent;
8+use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
9+use fuzzy::StringMatchCandidate;
10+use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
11+use language::{Buffer, CodeLabel};
12+use menu::Confirm;
13+use project::{
14+ debugger::session::{CompletionsQuery, OutputToken, Session},
15+ Completion,
16+};
17+use settings::Settings;
18+use std::{cell::RefCell, rc::Rc, usize};
19+use theme::ThemeSettings;
20+use ui::prelude::*;
21+
22+pub struct Console {
23+ console: Entity<Editor>,
24+ query_bar: Entity<Editor>,
25+ session: Entity<Session>,
26+ _subscriptions: Vec<Subscription>,
27+ variable_list: Entity<VariableList>,
28+ stack_frame_list: Entity<StackFrameList>,
29+ last_token: OutputToken,
30+ update_output_task: Task<()>,
31+}
32+
33+impl Console {
34+ pub fn new(
35+ session: Entity<Session>,
36+ stack_frame_list: Entity<StackFrameList>,
37+ variable_list: Entity<VariableList>,
38+ window: &mut Window,
39+ cx: &mut Context<Self>,
40+ ) -> Self {
41+ let console = cx.new(|cx| {
42+ let mut editor = Editor::multi_line(window, cx);
43+ editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
44+ editor.set_read_only(true);
45+ editor.set_show_gutter(true, cx);
46+ editor.set_show_runnables(false, cx);
47+ editor.set_show_breakpoints(false, cx);
48+ editor.set_show_code_actions(false, cx);
49+ editor.set_show_line_numbers(false, cx);
50+ editor.set_show_git_diff_gutter(false, cx);
51+ editor.set_autoindent(false);
52+ editor.set_input_enabled(false);
53+ editor.set_use_autoclose(false);
54+ editor.set_show_wrap_guides(false, cx);
55+ editor.set_show_indent_guides(false, cx);
56+ editor.set_show_edit_predictions(Some(false), window, cx);
57+ editor
58+ });
59+
60+ let this = cx.weak_entity();
61+ let query_bar = cx.new(|cx| {
62+ let mut editor = Editor::single_line(window, cx);
63+ editor.set_placeholder_text("Evaluate an expression", cx);
64+ editor.set_use_autoclose(false);
65+ editor.set_show_gutter(false, cx);
66+ editor.set_show_wrap_guides(false, cx);
67+ editor.set_show_indent_guides(false, cx);
68+ editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
69+
70+ editor
71+ });
72+
73+ let _subscriptions =
74+ vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
75+
76+ Self {
77+ session,
78+ console,
79+ query_bar,
80+ variable_list,
81+ _subscriptions,
82+ stack_frame_list,
83+ update_output_task: Task::ready(()),
84+ last_token: OutputToken(0),
85+ }
86+ }
87+
88+ #[cfg(any(test, feature = "test-support"))]
89+ pub fn editor(&self) -> &Entity<Editor> {
90+ &self.console
91+ }
92+
93+ #[cfg(any(test, feature = "test-support"))]
94+ pub fn query_bar(&self) -> &Entity<Editor> {
95+ &self.query_bar
96+ }
97+
98+ fn is_local(&self, cx: &Context<Self>) -> bool {
99+ self.session.read(cx).is_local()
100+ }
101+
102+ fn handle_stack_frame_list_events(
103+ &mut self,
104+ _: Entity<StackFrameList>,
105+ event: &StackFrameListEvent,
106+ cx: &mut Context<Self>,
107+ ) {
108+ match event {
109+ StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
110+ }
111+ }
112+
113+ pub fn add_messages<'a>(
114+ &mut self,
115+ events: impl Iterator<Item = &'a OutputEvent>,
116+ window: &mut Window,
117+ cx: &mut App,
118+ ) {
119+ self.console.update(cx, |console, cx| {
120+ let mut to_insert = String::default();
121+ for event in events {
122+ use std::fmt::Write;
123+
124+ _ = write!(to_insert, "{}\n", event.output.trim_end());
125+ }
126+
127+ console.set_read_only(false);
128+ console.move_to_end(&editor::actions::MoveToEnd, window, cx);
129+ console.insert(&to_insert, window, cx);
130+ console.set_read_only(true);
131+
132+ cx.notify();
133+ });
134+ }
135+
136+ pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
137+ let expression = self.query_bar.update(cx, |editor, cx| {
138+ let expression = editor.text(cx);
139+
140+ editor.clear(window, cx);
141+
142+ expression
143+ });
144+
145+ self.session.update(cx, |state, cx| {
146+ state.evaluate(
147+ expression,
148+ Some(dap::EvaluateArgumentsContext::Variables),
149+ self.stack_frame_list.read(cx).current_stack_frame_id(),
150+ None,
151+ cx,
152+ );
153+ });
154+ }
155+
156+ fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
157+ let settings = ThemeSettings::get_global(cx);
158+ let text_style = TextStyle {
159+ color: if self.console.read(cx).read_only(cx) {
160+ cx.theme().colors().text_disabled
161+ } else {
162+ cx.theme().colors().text
163+ },
164+ font_family: settings.buffer_font.family.clone(),
165+ font_features: settings.buffer_font.features.clone(),
166+ font_size: settings.buffer_font_size(cx).into(),
167+ font_weight: settings.buffer_font.weight,
168+ line_height: relative(settings.buffer_line_height.value()),
169+ ..Default::default()
170+ };
171+
172+ EditorElement::new(
173+ &self.console,
174+ EditorStyle {
175+ background: cx.theme().colors().editor_background,
176+ local_player: cx.theme().players().local(),
177+ text: text_style,
178+ ..Default::default()
179+ },
180+ )
181+ }
182+
183+ fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
184+ let settings = ThemeSettings::get_global(cx);
185+ let text_style = TextStyle {
186+ color: if self.console.read(cx).read_only(cx) {
187+ cx.theme().colors().text_disabled
188+ } else {
189+ cx.theme().colors().text
190+ },
191+ font_family: settings.ui_font.family.clone(),
192+ font_features: settings.ui_font.features.clone(),
193+ font_fallbacks: settings.ui_font.fallbacks.clone(),
194+ font_size: TextSize::Editor.rems(cx).into(),
195+ font_weight: settings.ui_font.weight,
196+ line_height: relative(1.3),
197+ ..Default::default()
198+ };
199+
200+ EditorElement::new(
201+ &self.query_bar,
202+ EditorStyle {
203+ background: cx.theme().colors().editor_background,
204+ local_player: cx.theme().players().local(),
205+ text: text_style,
206+ ..Default::default()
207+ },
208+ )
209+ }
210+}
211+
212+impl Render for Console {
213+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
214+ let session = self.session.clone();
215+ let token = self.last_token;
216+ self.update_output_task = cx.spawn_in(window, move |this, mut cx| async move {
217+ _ = session.update_in(&mut cx, move |session, window, cx| {
218+ let (output, last_processed_token) = session.output(token);
219+
220+ _ = this.update(cx, |this, cx| {
221+ if last_processed_token == this.last_token {
222+ return;
223+ }
224+ this.add_messages(output, window, cx);
225+
226+ this.last_token = last_processed_token;
227+ });
228+ });
229+ });
230+
231+ v_flex()
232+ .key_context("DebugConsole")
233+ .on_action(cx.listener(Self::evaluate))
234+ .size_full()
235+ .child(self.render_console(cx))
236+ .when(self.is_local(cx), |this| {
237+ this.child(self.render_query_bar(cx))
238+ .pt(DynamicSpacing::Base04.rems(cx))
239+ })
240+ .border_2()
241+ }
242+}
243+
244+struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
245+
246+impl CompletionProvider for ConsoleQueryBarCompletionProvider {
247+ fn completions(
248+ &self,
249+ buffer: &Entity<Buffer>,
250+ buffer_position: language::Anchor,
251+ _trigger: editor::CompletionContext,
252+ _window: &mut Window,
253+ cx: &mut Context<Editor>,
254+ ) -> Task<Result<Option<Vec<Completion>>>> {
255+ let Some(console) = self.0.upgrade() else {
256+ return Task::ready(Ok(None));
257+ };
258+
259+ let support_completions = console
260+ .read(cx)
261+ .session
262+ .read(cx)
263+ .capabilities()
264+ .supports_completions_request
265+ .unwrap_or_default();
266+
267+ if support_completions {
268+ self.client_completions(&console, buffer, buffer_position, cx)
269+ } else {
270+ self.variable_list_completions(&console, buffer, buffer_position, cx)
271+ }
272+ }
273+
274+ fn resolve_completions(
275+ &self,
276+ _buffer: Entity<Buffer>,
277+ _completion_indices: Vec<usize>,
278+ _completions: Rc<RefCell<Box<[Completion]>>>,
279+ _cx: &mut Context<Editor>,
280+ ) -> gpui::Task<gpui::Result<bool>> {
281+ Task::ready(Ok(false))
282+ }
283+
284+ fn apply_additional_edits_for_completion(
285+ &self,
286+ _buffer: Entity<Buffer>,
287+ _completions: Rc<RefCell<Box<[Completion]>>>,
288+ _completion_index: usize,
289+ _push_to_history: bool,
290+ _cx: &mut Context<Editor>,
291+ ) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
292+ Task::ready(Ok(None))
293+ }
294+
295+ fn is_completion_trigger(
296+ &self,
297+ _buffer: &Entity<Buffer>,
298+ _position: language::Anchor,
299+ _text: &str,
300+ _trigger_in_words: bool,
301+ _cx: &mut Context<Editor>,
302+ ) -> bool {
303+ true
304+ }
305+}
306+
307+impl ConsoleQueryBarCompletionProvider {
308+ fn variable_list_completions(
309+ &self,
310+ console: &Entity<Console>,
311+ buffer: &Entity<Buffer>,
312+ buffer_position: language::Anchor,
313+ cx: &mut Context<Editor>,
314+ ) -> Task<Result<Option<Vec<Completion>>>> {
315+ let (variables, string_matches) = console.update(cx, |console, cx| {
316+ let mut variables = HashMap::default();
317+ let mut string_matches = Vec::default();
318+
319+ for variable in console.variable_list.update(cx, |variable_list, cx| {
320+ variable_list.completion_variables(cx)
321+ }) {
322+ if let Some(evaluate_name) = &variable.evaluate_name {
323+ variables.insert(evaluate_name.clone(), variable.value.clone());
324+ string_matches.push(StringMatchCandidate {
325+ id: 0,
326+ string: evaluate_name.clone(),
327+ char_bag: evaluate_name.chars().collect(),
328+ });
329+ }
330+
331+ variables.insert(variable.name.clone(), variable.value.clone());
332+
333+ string_matches.push(StringMatchCandidate {
334+ id: 0,
335+ string: variable.name.clone(),
336+ char_bag: variable.name.chars().collect(),
337+ });
338+ }
339+
340+ (variables, string_matches)
341+ });
342+
343+ let query = buffer.read(cx).text();
344+
345+ cx.spawn(|_, cx| async move {
346+ let matches = fuzzy::match_strings(
347+ &string_matches,
348+ &query,
349+ true,
350+ 10,
351+ &Default::default(),
352+ cx.background_executor().clone(),
353+ )
354+ .await;
355+
356+ Ok(Some(
357+ matches
358+ .iter()
359+ .filter_map(|string_match| {
360+ let variable_value = variables.get(&string_match.string)?;
361+
362+ Some(project::Completion {
363+ old_range: buffer_position..buffer_position,
364+ new_text: string_match.string.clone(),
365+ label: CodeLabel {
366+ filter_range: 0..string_match.string.len(),
367+ text: format!("{} {}", string_match.string.clone(), variable_value),
368+ runs: Vec::new(),
369+ },
370+ documentation: None,
371+ confirm: None,
372+ source: project::CompletionSource::Custom,
373+ })
374+ })
375+ .collect(),
376+ ))
377+ })
378+ }
379+
380+ fn client_completions(
381+ &self,
382+ console: &Entity<Console>,
383+ buffer: &Entity<Buffer>,
384+ buffer_position: language::Anchor,
385+ cx: &mut Context<Editor>,
386+ ) -> Task<Result<Option<Vec<Completion>>>> {
387+ let completion_task = console.update(cx, |console, cx| {
388+ console.session.update(cx, |state, cx| {
389+ let frame_id = console.stack_frame_list.read(cx).current_stack_frame_id();
390+
391+ state.completions(
392+ CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
393+ cx,
394+ )
395+ })
396+ });
397+
398+ cx.background_executor().spawn(async move {
399+ Ok(Some(
400+ completion_task
401+ .await?
402+ .iter()
403+ .map(|completion| project::Completion {
404+ old_range: buffer_position..buffer_position, // TODO(debugger): change this
405+ new_text: completion.text.clone().unwrap_or(completion.label.clone()),
406+ label: CodeLabel {
407+ filter_range: 0..completion.label.len(),
408+ text: completion.label.clone(),
409+ runs: Vec::new(),
410+ },
411+ documentation: None,
412+ confirm: None,
413+ source: project::CompletionSource::Custom,
414+ })
415+ .collect(),
416+ ))
417+ })
418+ }
419+}
crates/debugger_ui/src/session/running/loaded_source_list.rs+103−0
@@ -0,0 +1,103 @@
1+use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
2+use project::debugger::session::{Session, SessionEvent};
3+use ui::prelude::*;
4+use util::maybe;
5+
6+pub struct LoadedSourceList {
7+ list: ListState,
8+ invalidate: bool,
9+ focus_handle: FocusHandle,
10+ _subscription: Subscription,
11+ session: Entity<Session>,
12+}
13+
14+impl LoadedSourceList {
15+ pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
16+ let weak_entity = cx.weak_entity();
17+ let focus_handle = cx.focus_handle();
18+
19+ let list = ListState::new(
20+ 0,
21+ gpui::ListAlignment::Top,
22+ px(1000.),
23+ move |ix, _window, cx| {
24+ weak_entity
25+ .upgrade()
26+ .map(|loaded_sources| {
27+ loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx))
28+ })
29+ .unwrap_or(div().into_any())
30+ },
31+ );
32+
33+ let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
34+ SessionEvent::Stopped(_) | SessionEvent::LoadedSources => {
35+ this.invalidate = true;
36+ cx.notify();
37+ }
38+ _ => {}
39+ });
40+
41+ Self {
42+ list,
43+ session,
44+ focus_handle,
45+ _subscription,
46+ invalidate: true,
47+ }
48+ }
49+
50+ fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
51+ let Some(source) = maybe!({
52+ self.session
53+ .update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned())
54+ }) else {
55+ return Empty.into_any();
56+ };
57+
58+ v_flex()
59+ .rounded_md()
60+ .w_full()
61+ .group("")
62+ .p_1()
63+ .hover(|s| s.bg(cx.theme().colors().element_hover))
64+ .child(
65+ h_flex()
66+ .gap_0p5()
67+ .text_ui_sm(cx)
68+ .when_some(source.name.clone(), |this, name| this.child(name)),
69+ )
70+ .child(
71+ h_flex()
72+ .text_ui_xs(cx)
73+ .text_color(cx.theme().colors().text_muted)
74+ .when_some(source.path.clone(), |this, path| this.child(path)),
75+ )
76+ .into_any()
77+ }
78+}
79+
80+impl Focusable for LoadedSourceList {
81+ fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
82+ self.focus_handle.clone()
83+ }
84+}
85+
86+impl Render for LoadedSourceList {
87+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
88+ if self.invalidate {
89+ let len = self
90+ .session
91+ .update(cx, |session, cx| session.loaded_sources(cx).len());
92+ self.list.reset(len);
93+ self.invalidate = false;
94+ cx.notify();
95+ }
96+
97+ div()
98+ .track_focus(&self.focus_handle)
99+ .size_full()
100+ .p_1()
101+ .child(list(self.list.clone()).size_full())
102+ }
103+}
crates/debugger_ui/src/session/running/module_list.rs+183−0
@@ -0,0 +1,183 @@
1+use anyhow::anyhow;
2+use gpui::{
3+ list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, WeakEntity,
4+};
5+use project::{
6+ debugger::session::{Session, SessionEvent},
7+ ProjectItem as _, ProjectPath,
8+};
9+use std::{path::Path, sync::Arc};
10+use ui::prelude::*;
11+use util::maybe;
12+use workspace::Workspace;
13+
14+pub struct ModuleList {
15+ list: ListState,
16+ invalidate: bool,
17+ session: Entity<Session>,
18+ workspace: WeakEntity<Workspace>,
19+ focus_handle: FocusHandle,
20+ _subscription: Subscription,
21+}
22+
23+impl ModuleList {
24+ pub fn new(
25+ session: Entity<Session>,
26+ workspace: WeakEntity<Workspace>,
27+ cx: &mut Context<Self>,
28+ ) -> Self {
29+ let weak_entity = cx.weak_entity();
30+ let focus_handle = cx.focus_handle();
31+
32+ let list = ListState::new(
33+ 0,
34+ gpui::ListAlignment::Top,
35+ px(1000.),
36+ move |ix, _window, cx| {
37+ weak_entity
38+ .upgrade()
39+ .map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
40+ .unwrap_or(div().into_any())
41+ },
42+ );
43+
44+ let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
45+ SessionEvent::Stopped(_) | SessionEvent::Modules => {
46+ this.invalidate = true;
47+ cx.notify();
48+ }
49+ _ => {}
50+ });
51+
52+ Self {
53+ list,
54+ session,
55+ workspace,
56+ focus_handle,
57+ _subscription,
58+ invalidate: true,
59+ }
60+ }
61+
62+ fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
63+ cx.spawn_in(window, move |this, mut cx| async move {
64+ let (worktree, relative_path) = this
65+ .update(&mut cx, |this, cx| {
66+ this.workspace.update(cx, |workspace, cx| {
67+ workspace.project().update(cx, |this, cx| {
68+ this.find_or_create_worktree(&path, false, cx)
69+ })
70+ })
71+ })??
72+ .await?;
73+
74+ let buffer = this
75+ .update(&mut cx, |this, cx| {
76+ this.workspace.update(cx, |this, cx| {
77+ this.project().update(cx, |this, cx| {
78+ let worktree_id = worktree.read(cx).id();
79+ this.open_buffer(
80+ ProjectPath {
81+ worktree_id,
82+ path: relative_path.into(),
83+ },
84+ cx,
85+ )
86+ })
87+ })
88+ })??
89+ .await?;
90+
91+ this.update_in(&mut cx, |this, window, cx| {
92+ this.workspace.update(cx, |workspace, cx| {
93+ let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
94+ anyhow!("Could not select a stack frame for unnamed buffer")
95+ })?;
96+ anyhow::Ok(workspace.open_path_preview(
97+ project_path,
98+ None,
99+ false,
100+ true,
101+ true,
102+ window,
103+ cx,
104+ ))
105+ })
106+ })???
107+ .await?;
108+
109+ anyhow::Ok(())
110+ })
111+ .detach_and_log_err(cx);
112+ }
113+
114+ fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
115+ let Some(module) = maybe!({
116+ self.session
117+ .update(cx, |state, cx| state.modules(cx).get(ix).cloned())
118+ }) else {
119+ return Empty.into_any();
120+ };
121+
122+ v_flex()
123+ .rounded_md()
124+ .w_full()
125+ .group("")
126+ .id(("module-list", ix))
127+ .when(module.path.is_some(), |this| {
128+ this.on_click({
129+ let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
130+ cx.listener(move |this, _, window, cx| {
131+ if let Some(path) = path.as_ref() {
132+ this.open_module(path.clone(), window, cx);
133+ } else {
134+ log::error!("Wasn't able to find module path, but was still able to click on module list entry");
135+ }
136+ })
137+ })
138+ })
139+ .p_1()
140+ .hover(|s| s.bg(cx.theme().colors().element_hover))
141+ .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
142+ .child(
143+ h_flex()
144+ .text_ui_xs(cx)
145+ .text_color(cx.theme().colors().text_muted)
146+ .when_some(module.path.clone(), |this, path| this.child(path)),
147+ )
148+ .into_any()
149+ }
150+}
151+
152+#[cfg(any(test, feature = "test-support"))]
153+impl ModuleList {
154+ pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
155+ self.session
156+ .update(cx, |session, cx| session.modules(cx).to_vec())
157+ }
158+}
159+
160+impl Focusable for ModuleList {
161+ fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
162+ self.focus_handle.clone()
163+ }
164+}
165+
166+impl Render for ModuleList {
167+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
168+ if self.invalidate {
169+ let len = self
170+ .session
171+ .update(cx, |session, cx| session.modules(cx).len());
172+ self.list.reset(len);
173+ self.invalidate = false;
174+ cx.notify();
175+ }
176+
177+ div()
178+ .track_focus(&self.focus_handle)
179+ .size_full()
180+ .p_1()
181+ .child(list(self.list.clone()).size_full())
182+ }
183+}
crates/debugger_ui/src/session/running/stack_frame_list.rs+519−0
@@ -0,0 +1,519 @@
1+use std::path::Path;
2+use std::sync::Arc;
3+
4+use anyhow::{anyhow, Result};
5+use dap::StackFrameId;
6+use gpui::{
7+ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
8+ WeakEntity,
9+};
10+
11+use language::PointUtf16;
12+use project::debugger::session::{Session, SessionEvent, StackFrame};
13+use project::{ProjectItem, ProjectPath};
14+use ui::{prelude::*, Tooltip};
15+use util::ResultExt;
16+use workspace::Workspace;
17+
18+use super::RunningState;
19+
20+#[derive(Debug)]
21+pub enum StackFrameListEvent {
22+ SelectedStackFrameChanged(StackFrameId),
23+}
24+
25+pub struct StackFrameList {
26+ list: ListState,
27+ focus_handle: FocusHandle,
28+ _subscription: Subscription,
29+ session: Entity<Session>,
30+ state: WeakEntity<RunningState>,
31+ invalidate: bool,
32+ entries: Vec<StackFrameEntry>,
33+ workspace: WeakEntity<Workspace>,
34+ current_stack_frame_id: Option<StackFrameId>,
35+}
36+
37+#[allow(clippy::large_enum_variant)]
38+#[derive(Debug, PartialEq, Eq)]
39+pub enum StackFrameEntry {
40+ Normal(dap::StackFrame),
41+ Collapsed(Vec<dap::StackFrame>),
42+}
43+
44+impl StackFrameList {
45+ pub fn new(
46+ workspace: WeakEntity<Workspace>,
47+ session: Entity<Session>,
48+ state: WeakEntity<RunningState>,
49+ window: &mut Window,
50+ cx: &mut Context<Self>,
51+ ) -> Self {
52+ let weak_entity = cx.weak_entity();
53+ let focus_handle = cx.focus_handle();
54+
55+ let list = ListState::new(
56+ 0,
57+ gpui::ListAlignment::Top,
58+ px(1000.),
59+ move |ix, _window, cx| {
60+ weak_entity
61+ .upgrade()
62+ .map(|stack_frame_list| {
63+ stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
64+ })
65+ .unwrap_or(div().into_any())
66+ },
67+ );
68+
69+ let _subscription =
70+ cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
71+ SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
72+ this.refresh(cx);
73+ }
74+ _ => {}
75+ });
76+
77+ Self {
78+ list,
79+ session,
80+ workspace,
81+ focus_handle,
82+ state,
83+ _subscription,
84+ invalidate: true,
85+ entries: Default::default(),
86+ current_stack_frame_id: None,
87+ }
88+ }
89+
90+ #[cfg(any(test, feature = "test-support"))]
91+ pub fn entries(&self) -> &Vec<StackFrameEntry> {
92+ &self.entries
93+ }
94+
95+ #[cfg(any(test, feature = "test-support"))]
96+ pub fn flatten_entries(&self) -> Vec<dap::StackFrame> {
97+ self.entries
98+ .iter()
99+ .flat_map(|frame| match frame {
100+ StackFrameEntry::Normal(frame) => vec![frame.clone()],
101+ StackFrameEntry::Collapsed(frames) => frames.clone(),
102+ })
103+ .collect::<Vec<_>>()
104+ }
105+
106+ fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
107+ self.state
108+ .read_with(cx, |state, _| state.thread_id)
109+ .log_err()
110+ .flatten()
111+ .map(|thread_id| {
112+ self.session
113+ .update(cx, |this, cx| this.stack_frames(thread_id, cx))
114+ })
115+ .unwrap_or_default()
116+ }
117+
118+ #[cfg(any(test, feature = "test-support"))]
119+ pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
120+ self.stack_frames(cx)
121+ .into_iter()
122+ .map(|stack_frame| stack_frame.dap.clone())
123+ .collect()
124+ }
125+
126+ pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
127+ self.stack_frames(cx)
128+ .first()
129+ .map(|stack_frame| stack_frame.dap.id)
130+ .unwrap_or(0)
131+ }
132+
133+ pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
134+ self.current_stack_frame_id
135+ }
136+
137+ pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
138+ self.invalidate = true;
139+ self.entries.clear();
140+ cx.notify();
141+ }
142+
143+ pub fn build_entries(
144+ &mut self,
145+ select_first_stack_frame: bool,
146+ window: &mut Window,
147+ cx: &mut Context<Self>,
148+ ) {
149+ let mut entries = Vec::new();
150+ let mut collapsed_entries = Vec::new();
151+ let mut current_stack_frame = None;
152+
153+ let stack_frames = self.stack_frames(cx);
154+ for stack_frame in &stack_frames {
155+ match stack_frame.dap.presentation_hint {
156+ Some(dap::StackFramePresentationHint::Deemphasize) => {
157+ collapsed_entries.push(stack_frame.dap.clone());
158+ }
159+ _ => {
160+ let collapsed_entries = std::mem::take(&mut collapsed_entries);
161+ if !collapsed_entries.is_empty() {
162+ entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
163+ }
164+
165+ current_stack_frame.get_or_insert(&stack_frame.dap);
166+ entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
167+ }
168+ }
169+ }
170+
171+ let collapsed_entries = std::mem::take(&mut collapsed_entries);
172+ if !collapsed_entries.is_empty() {
173+ entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
174+ }
175+
176+ std::mem::swap(&mut self.entries, &mut entries);
177+ self.list.reset(self.entries.len());
178+
179+ if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
180+ {
181+ self.select_stack_frame(current_stack_frame, true, window, cx)
182+ .detach_and_log_err(cx);
183+ }
184+
185+ cx.notify();
186+ }
187+
188+ pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
189+ if let Some(current_stack_frame_id) = self.current_stack_frame_id {
190+ let frame = self
191+ .entries
192+ .iter()
193+ .find_map(|entry| match entry {
194+ StackFrameEntry::Normal(dap) => {
195+ if dap.id == current_stack_frame_id {
196+ Some(dap)
197+ } else {
198+ None
199+ }
200+ }
201+ StackFrameEntry::Collapsed(daps) => {
202+ daps.iter().find(|dap| dap.id == current_stack_frame_id)
203+ }
204+ })
205+ .cloned();
206+
207+ if let Some(frame) = frame.as_ref() {
208+ self.select_stack_frame(frame, true, window, cx)
209+ .detach_and_log_err(cx);
210+ }
211+ }
212+ }
213+
214+ pub fn select_stack_frame(
215+ &mut self,
216+ stack_frame: &dap::StackFrame,
217+ go_to_stack_frame: bool,
218+ window: &Window,
219+ cx: &mut Context<Self>,
220+ ) -> Task<Result<()>> {
221+ self.current_stack_frame_id = Some(stack_frame.id);
222+
223+ cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
224+ stack_frame.id,
225+ ));
226+ cx.notify();
227+
228+ if !go_to_stack_frame {
229+ return Task::ready(Ok(()));
230+ };
231+
232+ let row = (stack_frame.line.saturating_sub(1)) as u32;
233+
234+ let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
235+ return Task::ready(Err(anyhow!("Project path not found")));
236+ };
237+
238+ cx.spawn_in(window, move |this, mut cx| async move {
239+ let (worktree, relative_path) = this
240+ .update(&mut cx, |this, cx| {
241+ this.workspace.update(cx, |workspace, cx| {
242+ workspace.project().update(cx, |this, cx| {
243+ this.find_or_create_worktree(&abs_path, false, cx)
244+ })
245+ })
246+ })??
247+ .await?;
248+ let buffer = this
249+ .update(&mut cx, |this, cx| {
250+ this.workspace.update(cx, |this, cx| {
251+ this.project().update(cx, |this, cx| {
252+ let worktree_id = worktree.read(cx).id();
253+ this.open_buffer(
254+ ProjectPath {
255+ worktree_id,
256+ path: relative_path.into(),
257+ },
258+ cx,
259+ )
260+ })
261+ })
262+ })??
263+ .await?;
264+ let position = buffer.update(&mut cx, |this, _| {
265+ this.snapshot().anchor_after(PointUtf16::new(row, 0))
266+ })?;
267+ this.update_in(&mut cx, |this, window, cx| {
268+ this.workspace.update(cx, |workspace, cx| {
269+ let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
270+ anyhow!("Could not select a stack frame for unnamed buffer")
271+ })?;
272+ anyhow::Ok(workspace.open_path_preview(
273+ project_path,
274+ None,
275+ false,
276+ true,
277+ true,
278+ window,
279+ cx,
280+ ))
281+ })
282+ })???
283+ .await?;
284+
285+ this.update(&mut cx, |this, cx| {
286+ this.workspace.update(cx, |workspace, cx| {
287+ let breakpoint_store = workspace.project().read(cx).breakpoint_store();
288+
289+ breakpoint_store.update(cx, |store, cx| {
290+ store.set_active_position(
291+ (this.session.read(cx).session_id(), abs_path, position),
292+ cx,
293+ );
294+ })
295+ })
296+ })?
297+ })
298+ }
299+
300+ fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
301+ stack_frame.source.as_ref().and_then(|s| {
302+ s.path
303+ .as_deref()
304+ .map(|path| Arc::<Path>::from(Path::new(path)))
305+ })
306+ }
307+
308+ pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
309+ self.session.update(cx, |state, cx| {
310+ state.restart_stack_frame(stack_frame_id, cx)
311+ });
312+ }
313+
314+ fn render_normal_entry(
315+ &self,
316+ stack_frame: &dap::StackFrame,
317+ cx: &mut Context<Self>,
318+ ) -> AnyElement {
319+ let source = stack_frame.source.clone();
320+ let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
321+
322+ let formatted_path = format!(
323+ "{}:{}",
324+ source.clone().and_then(|s| s.name).unwrap_or_default(),
325+ stack_frame.line,
326+ );
327+
328+ let supports_frame_restart = self
329+ .session
330+ .read(cx)
331+ .capabilities()
332+ .supports_restart_frame
333+ .unwrap_or_default();
334+
335+ let origin = stack_frame
336+ .source
337+ .to_owned()
338+ .and_then(|source| source.origin);
339+
340+ h_flex()
341+ .rounded_md()
342+ .justify_between()
343+ .w_full()
344+ .group("")
345+ .id(("stack-frame", stack_frame.id))
346+ .tooltip({
347+ let formatted_path = formatted_path.clone();
348+ move |_window, app| {
349+ app.new(|_| {
350+ let mut tooltip = Tooltip::new(formatted_path.clone());
351+
352+ if let Some(origin) = &origin {
353+ tooltip = tooltip.meta(origin);
354+ }
355+
356+ tooltip
357+ })
358+ .into()
359+ }
360+ })
361+ .p_1()
362+ .when(is_selected_frame, |this| {
363+ this.bg(cx.theme().colors().element_hover)
364+ })
365+ .on_click(cx.listener({
366+ let stack_frame = stack_frame.clone();
367+ move |this, _, window, cx| {
368+ this.select_stack_frame(&stack_frame, true, window, cx)
369+ .detach_and_log_err(cx);
370+ }
371+ }))
372+ .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
373+ .child(
374+ v_flex()
375+ .child(
376+ h_flex()
377+ .gap_0p5()
378+ .text_ui_sm(cx)
379+ .truncate()
380+ .child(stack_frame.name.clone())
381+ .child(formatted_path),
382+ )
383+ .child(
384+ h_flex()
385+ .text_ui_xs(cx)
386+ .truncate()
387+ .text_color(cx.theme().colors().text_muted)
388+ .when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
389+ ),
390+ )
391+ .when(
392+ supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
393+ |this| {
394+ this.child(
395+ h_flex()
396+ .id(("restart-stack-frame", stack_frame.id))
397+ .visible_on_hover("")
398+ .absolute()
399+ .right_2()
400+ .overflow_hidden()
401+ .rounded_md()
402+ .border_1()
403+ .border_color(cx.theme().colors().element_selected)
404+ .bg(cx.theme().colors().element_background)
405+ .hover(|style| {
406+ style
407+ .bg(cx.theme().colors().ghost_element_hover)
408+ .cursor_pointer()
409+ })
410+ .child(
411+ IconButton::new(
412+ ("restart-stack-frame", stack_frame.id),
413+ IconName::DebugRestart,
414+ )
415+ .icon_size(IconSize::Small)
416+ .on_click(cx.listener({
417+ let stack_frame_id = stack_frame.id;
418+ move |this, _, _window, cx| {
419+ this.restart_stack_frame(stack_frame_id, cx);
420+ }
421+ }))
422+ .tooltip(move |window, cx| {
423+ Tooltip::text("Restart Stack Frame")(window, cx)
424+ }),
425+ ),
426+ )
427+ },
428+ )
429+ .into_any()
430+ }
431+
432+ pub fn expand_collapsed_entry(
433+ &mut self,
434+ ix: usize,
435+ stack_frames: &Vec<dap::StackFrame>,
436+ cx: &mut Context<Self>,
437+ ) {
438+ self.entries.splice(
439+ ix..ix + 1,
440+ stack_frames
441+ .iter()
442+ .map(|frame| StackFrameEntry::Normal(frame.clone())),
443+ );
444+ self.list.reset(self.entries.len());
445+ cx.notify();
446+ }
447+
448+ fn render_collapsed_entry(
449+ &self,
450+ ix: usize,
451+ stack_frames: &Vec<dap::StackFrame>,
452+ cx: &mut Context<Self>,
453+ ) -> AnyElement {
454+ let first_stack_frame = &stack_frames[0];
455+
456+ h_flex()
457+ .rounded_md()
458+ .justify_between()
459+ .w_full()
460+ .group("")
461+ .id(("stack-frame", first_stack_frame.id))
462+ .p_1()
463+ .on_click(cx.listener({
464+ let stack_frames = stack_frames.clone();
465+ move |this, _, _window, cx| {
466+ this.expand_collapsed_entry(ix, &stack_frames, cx);
467+ }
468+ }))
469+ .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
470+ .child(
471+ v_flex()
472+ .text_ui_sm(cx)
473+ .truncate()
474+ .text_color(cx.theme().colors().text_muted)
475+ .child(format!(
476+ "Show {} more{}",
477+ stack_frames.len(),
478+ first_stack_frame
479+ .source
480+ .as_ref()
481+ .and_then(|source| source.origin.as_ref())
482+ .map_or(String::new(), |origin| format!(": {}", origin))
483+ )),
484+ )
485+ .into_any()
486+ }
487+
488+ fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
489+ match &self.entries[ix] {
490+ StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
491+ StackFrameEntry::Collapsed(stack_frames) => {
492+ self.render_collapsed_entry(ix, stack_frames, cx)
493+ }
494+ }
495+ }
496+}
497+
498+impl Render for StackFrameList {
499+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
500+ if self.invalidate {
501+ self.build_entries(self.entries.is_empty(), window, cx);
502+ self.invalidate = false;
503+ cx.notify();
504+ }
505+
506+ div()
507+ .size_full()
508+ .p_1()
509+ .child(list(self.list.clone()).size_full())
510+ }
511+}
512+
513+impl Focusable for StackFrameList {
514+ fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
515+ self.focus_handle.clone()
516+ }
517+}
518+
519+impl EventEmitter<StackFrameListEvent> for StackFrameList {}
crates/debugger_ui/src/session/running/variable_list.rs+946−0
@@ -0,0 +1,946 @@
1+use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
2+use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
3+use editor::Editor;
4+use gpui::{
5+ actions, anchored, deferred, uniform_list, AnyElement, ClickEvent, ClipboardItem, Context,
6+ DismissEvent, Entity, FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point,
7+ Stateful, Subscription, TextStyleRefinement, UniformListScrollHandle,
8+};
9+use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
10+use project::debugger::session::{Session, SessionEvent};
11+use std::{collections::HashMap, ops::Range, sync::Arc};
12+use ui::{prelude::*, ContextMenu, ListItem, Scrollbar, ScrollbarState};
13+use util::{debug_panic, maybe};
14+
15+actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
16+
17+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18+pub(crate) struct EntryState {
19+ depth: usize,
20+ is_expanded: bool,
21+ parent_reference: VariableReference,
22+}
23+
24+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
25+pub(crate) struct EntryPath {
26+ pub leaf_name: Option<SharedString>,
27+ pub indices: Arc<[SharedString]>,
28+}
29+
30+impl EntryPath {
31+ fn for_scope(scope_name: impl Into<SharedString>) -> Self {
32+ Self {
33+ leaf_name: Some(scope_name.into()),
34+ indices: Arc::new([]),
35+ }
36+ }
37+
38+ fn with_name(&self, name: SharedString) -> Self {
39+ Self {
40+ leaf_name: Some(name),
41+ indices: self.indices.clone(),
42+ }
43+ }
44+
45+ /// Create a new child of this variable path
46+ fn with_child(&self, name: SharedString) -> Self {
47+ Self {
48+ leaf_name: None,
49+ indices: self
50+ .indices
51+ .iter()
52+ .cloned()
53+ .chain(std::iter::once(name))
54+ .collect(),
55+ }
56+ }
57+}
58+
59+#[derive(Debug, Clone, PartialEq)]
60+enum EntryKind {
61+ Variable(dap::Variable),
62+ Scope(dap::Scope),
63+}
64+
65+impl EntryKind {
66+ fn as_variable(&self) -> Option<&dap::Variable> {
67+ match self {
68+ EntryKind::Variable(dap) => Some(dap),
69+ _ => None,
70+ }
71+ }
72+
73+ fn as_scope(&self) -> Option<&dap::Scope> {
74+ match self {
75+ EntryKind::Scope(dap) => Some(dap),
76+ _ => None,
77+ }
78+ }
79+
80+ #[allow(dead_code)]
81+ fn name(&self) -> &str {
82+ match self {
83+ EntryKind::Variable(dap) => &dap.name,
84+ EntryKind::Scope(dap) => &dap.name,
85+ }
86+ }
87+}
88+
89+#[derive(Debug, Clone, PartialEq)]
90+struct ListEntry {
91+ dap_kind: EntryKind,
92+ path: EntryPath,
93+}
94+
95+impl ListEntry {
96+ fn as_variable(&self) -> Option<&dap::Variable> {
97+ self.dap_kind.as_variable()
98+ }
99+
100+ fn as_scope(&self) -> Option<&dap::Scope> {
101+ self.dap_kind.as_scope()
102+ }
103+
104+ fn item_id(&self) -> ElementId {
105+ use std::fmt::Write;
106+ let mut id = match &self.dap_kind {
107+ EntryKind::Variable(dap) => format!("variable-{}", dap.name),
108+ EntryKind::Scope(dap) => format!("scope-{}", dap.name),
109+ };
110+ for name in self.path.indices.iter() {
111+ _ = write!(id, "-{}", name);
112+ }
113+ SharedString::from(id).into()
114+ }
115+
116+ fn item_value_id(&self) -> ElementId {
117+ use std::fmt::Write;
118+ let mut id = match &self.dap_kind {
119+ EntryKind::Variable(dap) => format!("variable-{}", dap.name),
120+ EntryKind::Scope(dap) => format!("scope-{}", dap.name),
121+ };
122+ for name in self.path.indices.iter() {
123+ _ = write!(id, "-{}", name);
124+ }
125+ _ = write!(id, "-value");
126+ SharedString::from(id).into()
127+ }
128+}
129+
130+pub struct VariableList {
131+ entries: Vec<ListEntry>,
132+ entry_states: HashMap<EntryPath, EntryState>,
133+ selected_stack_frame_id: Option<StackFrameId>,
134+ list_handle: UniformListScrollHandle,
135+ scrollbar_state: ScrollbarState,
136+ session: Entity<Session>,
137+ selection: Option<EntryPath>,
138+ open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
139+ focus_handle: FocusHandle,
140+ edited_path: Option<(EntryPath, Entity<Editor>)>,
141+ disabled: bool,
142+ _subscriptions: Vec<Subscription>,
143+}
144+
145+impl VariableList {
146+ pub fn new(
147+ session: Entity<Session>,
148+ stack_frame_list: Entity<StackFrameList>,
149+ window: &mut Window,
150+ cx: &mut Context<Self>,
151+ ) -> Self {
152+ let focus_handle = cx.focus_handle();
153+
154+ let _subscriptions = vec![
155+ cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
156+ cx.subscribe(&session, |this, _, event, _| match event {
157+ SessionEvent::Stopped(_) => {
158+ this.selection.take();
159+ this.edited_path.take();
160+ this.selected_stack_frame_id.take();
161+ }
162+ _ => {}
163+ }),
164+ cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
165+ this.edited_path.take();
166+ cx.notify();
167+ }),
168+ ];
169+
170+ let list_state = UniformListScrollHandle::default();
171+
172+ Self {
173+ scrollbar_state: ScrollbarState::new(list_state.clone()),
174+ list_handle: list_state,
175+ session,
176+ focus_handle,
177+ _subscriptions,
178+ selected_stack_frame_id: None,
179+ selection: None,
180+ open_context_menu: None,
181+ disabled: false,
182+ edited_path: None,
183+ entries: Default::default(),
184+ entry_states: Default::default(),
185+ }
186+ }
187+
188+ pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
189+ let old_disabled = std::mem::take(&mut self.disabled);
190+ self.disabled = disabled;
191+ if old_disabled != disabled {
192+ cx.notify();
193+ }
194+ }
195+
196+ fn build_entries(&mut self, cx: &mut Context<Self>) {
197+ let Some(stack_frame_id) = self.selected_stack_frame_id else {
198+ return;
199+ };
200+
201+ let mut entries = vec![];
202+ let scopes: Vec<_> = self.session.update(cx, |session, cx| {
203+ session.scopes(stack_frame_id, cx).iter().cloned().collect()
204+ });
205+
206+ let mut contains_local_scope = false;
207+
208+ let mut stack = scopes
209+ .into_iter()
210+ .rev()
211+ .filter(|scope| {
212+ if scope
213+ .presentation_hint
214+ .as_ref()
215+ .map(|hint| *hint == ScopePresentationHint::Locals)
216+ .unwrap_or(scope.name.to_lowercase().starts_with("local"))
217+ {
218+ contains_local_scope = true;
219+ }
220+
221+ self.session.update(cx, |session, cx| {
222+ session.variables(scope.variables_reference, cx).len() > 0
223+ })
224+ })
225+ .map(|scope| {
226+ (
227+ scope.variables_reference,
228+ scope.variables_reference,
229+ EntryPath::for_scope(&scope.name),
230+ EntryKind::Scope(scope),
231+ )
232+ })
233+ .collect::<Vec<_>>();
234+
235+ let scopes_count = stack.len();
236+
237+ while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
238+ {
239+ match &dap_kind {
240+ EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
241+ EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
242+ }
243+
244+ let var_state = self
245+ .entry_states
246+ .entry(path.clone())
247+ .and_modify(|state| {
248+ state.parent_reference = container_reference;
249+ })
250+ .or_insert(EntryState {
251+ depth: path.indices.len(),
252+ is_expanded: dap_kind.as_scope().is_some_and(|scope| {
253+ (scopes_count == 1 && !contains_local_scope)
254+ || scope
255+ .presentation_hint
256+ .as_ref()
257+ .map(|hint| *hint == ScopePresentationHint::Locals)
258+ .unwrap_or(scope.name.to_lowercase().starts_with("local"))
259+ }),
260+ parent_reference: container_reference,
261+ });
262+
263+ entries.push(ListEntry {
264+ dap_kind,
265+ path: path.clone(),
266+ });
267+
268+ if var_state.is_expanded {
269+ let children = self
270+ .session
271+ .update(cx, |session, cx| session.variables(variables_reference, cx));
272+ stack.extend(children.into_iter().rev().map(|child| {
273+ (
274+ variables_reference,
275+ child.variables_reference,
276+ path.with_child(child.name.clone().into()),
277+ EntryKind::Variable(child),
278+ )
279+ }));
280+ }
281+ }
282+
283+ self.entries = entries;
284+ cx.notify();
285+ }
286+
287+ fn handle_stack_frame_list_events(
288+ &mut self,
289+ _: Entity<StackFrameList>,
290+ event: &StackFrameListEvent,
291+ cx: &mut Context<Self>,
292+ ) {
293+ match event {
294+ StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
295+ self.selected_stack_frame_id = Some(*stack_frame_id);
296+ cx.notify();
297+ }
298+ }
299+ }
300+
301+ pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
302+ self.entries
303+ .iter()
304+ .filter_map(|entry| match &entry.dap_kind {
305+ EntryKind::Variable(dap) => Some(dap.clone()),
306+ EntryKind::Scope(_) => None,
307+ })
308+ .collect()
309+ }
310+
311+ fn render_entries(
312+ &mut self,
313+ ix: Range<usize>,
314+ window: &mut Window,
315+ cx: &mut Context<Self>,
316+ ) -> Vec<AnyElement> {
317+ ix.into_iter()
318+ .filter_map(|ix| {
319+ let (entry, state) = self
320+ .entries
321+ .get(ix)
322+ .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
323+
324+ match &entry.dap_kind {
325+ EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
326+ EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
327+ }
328+ })
329+ .collect()
330+ }
331+
332+ pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
333+ let Some(entry) = self.entry_states.get_mut(var_path) else {
334+ log::error!("Could not find variable list entry state to toggle");
335+ return;
336+ };
337+
338+ entry.is_expanded = !entry.is_expanded;
339+ cx.notify();
340+ }
341+
342+ fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
343+ self.cancel_variable_edit(&Default::default(), window, cx);
344+ if let Some(variable) = self.entries.first() {
345+ self.selection = Some(variable.path.clone());
346+ cx.notify();
347+ }
348+ }
349+
350+ fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
351+ self.cancel_variable_edit(&Default::default(), window, cx);
352+ if let Some(variable) = self.entries.last() {
353+ self.selection = Some(variable.path.clone());
354+ cx.notify();
355+ }
356+ }
357+
358+ fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
359+ self.cancel_variable_edit(&Default::default(), window, cx);
360+ if let Some(selection) = &self.selection {
361+ if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
362+ if &var.path == selection {
363+ Some(ix.saturating_sub(1))
364+ } else {
365+ None
366+ }
367+ }) {
368+ if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
369+ self.selection = Some(new_selection);
370+ cx.notify();
371+ } else {
372+ self.select_first(&SelectFirst, window, cx);
373+ }
374+ }
375+ } else {
376+ self.select_first(&SelectFirst, window, cx);
377+ }
378+ }
379+
380+ fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
381+ self.cancel_variable_edit(&Default::default(), window, cx);
382+ if let Some(selection) = &self.selection {
383+ if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
384+ if &var.path == selection {
385+ Some(ix.saturating_add(1))
386+ } else {
387+ None
388+ }
389+ }) {
390+ if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
391+ self.selection = Some(new_selection);
392+ cx.notify();
393+ } else {
394+ self.select_first(&SelectFirst, window, cx);
395+ }
396+ }
397+ } else {
398+ self.select_first(&SelectFirst, window, cx);
399+ }
400+ }
401+
402+ fn cancel_variable_edit(
403+ &mut self,
404+ _: &menu::Cancel,
405+ window: &mut Window,
406+ cx: &mut Context<Self>,
407+ ) {
408+ self.edited_path.take();
409+ self.focus_handle.focus(window);
410+ cx.notify();
411+ }
412+
413+ fn confirm_variable_edit(
414+ &mut self,
415+ _: &menu::Confirm,
416+ _window: &mut Window,
417+ cx: &mut Context<Self>,
418+ ) {
419+ let res = maybe!({
420+ let (var_path, editor) = self.edited_path.take()?;
421+ let state = self.entry_states.get(&var_path)?;
422+ let variables_reference = state.parent_reference;
423+ let name = var_path.leaf_name?;
424+ let value = editor.read(cx).text(cx);
425+
426+ self.session.update(cx, |session, cx| {
427+ session.set_variable_value(variables_reference, name.into(), value, cx)
428+ });
429+ Some(())
430+ });
431+
432+ if res.is_none() {
433+ log::error!("Couldn't confirm variable edit because variable doesn't have a leaf name or a parent reference id");
434+ }
435+ }
436+
437+ fn collapse_selected_entry(
438+ &mut self,
439+ _: &CollapseSelectedEntry,
440+ _window: &mut Window,
441+ cx: &mut Context<Self>,
442+ ) {
443+ if let Some(ref selected_entry) = self.selection {
444+ let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
445+ debug_panic!("Trying to toggle variable in variable list that has an no state");
446+ return;
447+ };
448+
449+ entry_state.is_expanded = false;
450+ cx.notify();
451+ }
452+ }
453+
454+ fn expand_selected_entry(
455+ &mut self,
456+ _: &ExpandSelectedEntry,
457+ _window: &mut Window,
458+ cx: &mut Context<Self>,
459+ ) {
460+ if let Some(ref selected_entry) = self.selection {
461+ let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
462+ debug_panic!("Trying to toggle variable in variable list that has an no state");
463+ return;
464+ };
465+
466+ entry_state.is_expanded = true;
467+ cx.notify();
468+ }
469+ }
470+
471+ fn deploy_variable_context_menu(
472+ &mut self,
473+ variable: ListEntry,
474+ position: Point<Pixels>,
475+ window: &mut Window,
476+ cx: &mut Context<Self>,
477+ ) {
478+ let Some(dap_var) = variable.as_variable() else {
479+ debug_panic!("Trying to open variable context menu on a scope");
480+ return;
481+ };
482+
483+ let variable_value = dap_var.value.clone();
484+ let variable_name = dap_var.name.clone();
485+ let this = cx.entity().clone();
486+
487+ let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
488+ menu.entry("Copy name", None, move |_, cx| {
489+ cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone()))
490+ })
491+ .entry("Copy value", None, {
492+ let variable_value = variable_value.clone();
493+ move |_, cx| {
494+ cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone()))
495+ }
496+ })
497+ .entry("Set value", None, move |window, cx| {
498+ this.update(cx, |variable_list, cx| {
499+ let editor = Self::create_variable_editor(&variable_value, window, cx);
500+ variable_list.edited_path = Some((variable.path.clone(), editor));
501+
502+ cx.notify();
503+ });
504+ })
505+ });
506+
507+ cx.focus_view(&context_menu, window);
508+ let subscription = cx.subscribe_in(
509+ &context_menu,
510+ window,
511+ |this, _, _: &DismissEvent, window, cx| {
512+ if this.open_context_menu.as_ref().is_some_and(|context_menu| {
513+ context_menu.0.focus_handle(cx).contains_focused(window, cx)
514+ }) {
515+ cx.focus_self(window);
516+ }
517+ this.open_context_menu.take();
518+ cx.notify();
519+ },
520+ );
521+
522+ self.open_context_menu = Some((context_menu, position, subscription));
523+ }
524+
525+ #[track_caller]
526+ #[cfg(any(test, feature = "test-support"))]
527+ pub fn assert_visual_entries(&self, expected: Vec<&str>) {
528+ const INDENT: &'static str = " ";
529+
530+ let entries = &self.entries;
531+ let mut visual_entries = Vec::with_capacity(entries.len());
532+ for entry in entries {
533+ let state = self
534+ .entry_states
535+ .get(&entry.path)
536+ .expect("If there's a variable entry there has to be a state that goes with it");
537+
538+ visual_entries.push(format!(
539+ "{}{} {}{}",
540+ INDENT.repeat(state.depth - 1),
541+ if state.is_expanded { "v" } else { ">" },
542+ entry.dap_kind.name(),
543+ if self.selection.as_ref() == Some(&entry.path) {
544+ " <=== selected"
545+ } else {
546+ ""
547+ }
548+ ));
549+ }
550+
551+ pretty_assertions::assert_eq!(expected, visual_entries);
552+ }
553+
554+ #[track_caller]
555+ #[cfg(any(test, feature = "test-support"))]
556+ pub fn scopes(&self) -> Vec<dap::Scope> {
557+ self.entries
558+ .iter()
559+ .filter_map(|entry| match &entry.dap_kind {
560+ EntryKind::Scope(scope) => Some(scope),
561+ _ => None,
562+ })
563+ .cloned()
564+ .collect()
565+ }
566+
567+ #[track_caller]
568+ #[cfg(any(test, feature = "test-support"))]
569+ pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
570+ let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
571+ let mut idx = 0;
572+
573+ for entry in self.entries.iter() {
574+ match &entry.dap_kind {
575+ EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
576+ EntryKind::Scope(scope) => {
577+ if scopes.len() > 0 {
578+ idx += 1;
579+ }
580+
581+ scopes.push((scope.clone(), Vec::new()));
582+ }
583+ }
584+ }
585+
586+ scopes
587+ }
588+
589+ #[track_caller]
590+ #[cfg(any(test, feature = "test-support"))]
591+ pub fn variables(&self) -> Vec<dap::Variable> {
592+ self.entries
593+ .iter()
594+ .filter_map(|entry| match &entry.dap_kind {
595+ EntryKind::Variable(variable) => Some(variable),
596+ _ => None,
597+ })
598+ .cloned()
599+ .collect()
600+ }
601+
602+ fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
603+ let editor = cx.new(|cx| {
604+ let mut editor = Editor::single_line(window, cx);
605+
606+ let refinement = TextStyleRefinement {
607+ font_size: Some(
608+ TextSize::XSmall
609+ .rems(cx)
610+ .to_pixels(window.rem_size())
611+ .into(),
612+ ),
613+ ..Default::default()
614+ };
615+ editor.set_text_style_refinement(refinement);
616+ editor.set_text(default, window, cx);
617+ editor.select_all(&editor::actions::SelectAll, window, cx);
618+ editor
619+ });
620+ editor.focus_handle(cx).focus(window);
621+ editor
622+ }
623+
624+ fn render_scope(
625+ &self,
626+ entry: &ListEntry,
627+ state: EntryState,
628+ cx: &mut Context<Self>,
629+ ) -> AnyElement {
630+ let Some(scope) = entry.as_scope() else {
631+ debug_panic!("Called render scope on non scope variable list entry variant");
632+ return div().into_any_element();
633+ };
634+
635+ let var_ref = scope.variables_reference;
636+ let is_selected = self
637+ .selection
638+ .as_ref()
639+ .is_some_and(|selection| selection == &entry.path);
640+
641+ let colors = get_entry_color(cx);
642+ let bg_hover_color = if !is_selected {
643+ colors.hover
644+ } else {
645+ colors.default
646+ };
647+ let border_color = if is_selected {
648+ colors.marked_active
649+ } else {
650+ colors.default
651+ };
652+
653+ div()
654+ .id(var_ref as usize)
655+ .group("variable_list_entry")
656+ .border_1()
657+ .border_r_2()
658+ .border_color(border_color)
659+ .flex()
660+ .w_full()
661+ .h_full()
662+ .hover(|style| style.bg(bg_hover_color))
663+ .on_click(cx.listener({
664+ move |_this, _, _window, cx| {
665+ cx.notify();
666+ }
667+ }))
668+ .child(
669+ ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
670+ .selectable(false)
671+ .indent_level(state.depth + 1)
672+ .indent_step_size(px(20.))
673+ .always_show_disclosure_icon(true)
674+ .toggle(state.is_expanded)
675+ .on_toggle({
676+ let var_path = entry.path.clone();
677+ cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
678+ })
679+ .child(div().text_ui(cx).w_full().child(scope.name.clone())),
680+ )
681+ .into_any()
682+ }
683+
684+ fn render_variable(
685+ &self,
686+ variable: &ListEntry,
687+ state: EntryState,
688+ window: &mut Window,
689+ cx: &mut Context<Self>,
690+ ) -> AnyElement {
691+ let dap = match &variable.dap_kind {
692+ EntryKind::Variable(dap) => dap,
693+ EntryKind::Scope(_) => {
694+ debug_panic!("Called render variable on variable list entry kind scope");
695+ return div().into_any_element();
696+ }
697+ };
698+
699+ let syntax_color_for = |name| cx.theme().syntax().get(name).color;
700+ let variable_name_color = match &dap
701+ .presentation_hint
702+ .as_ref()
703+ .and_then(|hint| hint.kind.as_ref())
704+ .unwrap_or(&VariablePresentationHintKind::Unknown)
705+ {
706+ VariablePresentationHintKind::Class
707+ | VariablePresentationHintKind::BaseClass
708+ | VariablePresentationHintKind::InnerClass
709+ | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
710+ VariablePresentationHintKind::Data => syntax_color_for("variable"),
711+ VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
712+ };
713+ let variable_color = syntax_color_for("variable.special");
714+
715+ let var_ref = dap.variables_reference;
716+ let colors = get_entry_color(cx);
717+ let is_selected = self
718+ .selection
719+ .as_ref()
720+ .is_some_and(|selected_path| *selected_path == variable.path);
721+
722+ let bg_hover_color = if !is_selected {
723+ colors.hover
724+ } else {
725+ colors.default
726+ };
727+ let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
728+ colors.marked_active
729+ } else {
730+ colors.default
731+ };
732+ let path = variable.path.clone();
733+ div()
734+ .id(variable.item_id())
735+ .group("variable_list_entry")
736+ .border_1()
737+ .border_r_2()
738+ .border_color(border_color)
739+ .h_4()
740+ .size_full()
741+ .hover(|style| style.bg(bg_hover_color))
742+ .on_click(cx.listener({
743+ move |this, _, _window, cx| {
744+ this.selection = Some(path.clone());
745+ cx.notify();
746+ }
747+ }))
748+ .child(
749+ ListItem::new(SharedString::from(format!(
750+ "variable-item-{}-{}",
751+ dap.name, state.depth
752+ )))
753+ .disabled(self.disabled)
754+ .selectable(false)
755+ .indent_level(state.depth + 1_usize)
756+ .indent_step_size(px(20.))
757+ .always_show_disclosure_icon(true)
758+ .when(var_ref > 0, |list_item| {
759+ list_item.toggle(state.is_expanded).on_toggle(cx.listener({
760+ let var_path = variable.path.clone();
761+ move |this, _, _, cx| {
762+ this.session.update(cx, |session, cx| {
763+ session.variables(var_ref, cx);
764+ });
765+
766+ this.toggle_entry(&var_path, cx);
767+ }
768+ }))
769+ })
770+ .on_secondary_mouse_down(cx.listener({
771+ let variable = variable.clone();
772+ move |this, event: &MouseDownEvent, window, cx| {
773+ this.deploy_variable_context_menu(
774+ variable.clone(),
775+ event.position,
776+ window,
777+ cx,
778+ )
779+ }
780+ }))
781+ .child(
782+ h_flex()
783+ .gap_1()
784+ .text_ui_sm(cx)
785+ .w_full()
786+ .child(
787+ Label::new(&dap.name).when_some(variable_name_color, |this, color| {
788+ this.color(Color::from(color))
789+ }),
790+ )
791+ .when(!dap.value.is_empty(), |this| {
792+ this.child(div().w_full().id(variable.item_value_id()).map(|this| {
793+ if let Some((_, editor)) = self
794+ .edited_path
795+ .as_ref()
796+ .filter(|(path, _)| path == &variable.path)
797+ {
798+ this.child(div().size_full().px_2().child(editor.clone()))
799+ } else {
800+ this.text_color(cx.theme().colors().text_muted)
801+ .when(
802+ !self.disabled
803+ && self
804+ .session
805+ .read(cx)
806+ .capabilities()
807+ .supports_set_variable
808+ .unwrap_or_default(),
809+ |this| {
810+ let path = variable.path.clone();
811+ let variable_value = dap.value.clone();
812+ this.on_click(cx.listener(
813+ move |this, click: &ClickEvent, window, cx| {
814+ if click.down.click_count < 2 {
815+ return;
816+ }
817+ let editor = Self::create_variable_editor(
818+ &variable_value,
819+ window,
820+ cx,
821+ );
822+ this.edited_path =
823+ Some((path.clone(), editor));
824+
825+ cx.notify();
826+ },
827+ ))
828+ },
829+ )
830+ .child(
831+ Label::new(format!("= {}", &dap.value))
832+ .single_line()
833+ .truncate()
834+ .size(LabelSize::Small)
835+ .when_some(variable_color, |this, color| {
836+ this.color(Color::from(color))
837+ }),
838+ )
839+ }
840+ }))
841+ }),
842+ ),
843+ )
844+ .into_any()
845+ }
846+
847+ fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
848+ div()
849+ .occlude()
850+ .id("variable-list-vertical-scrollbar")
851+ .on_mouse_move(cx.listener(|_, _, _, cx| {
852+ cx.notify();
853+ cx.stop_propagation()
854+ }))
855+ .on_hover(|_, _, cx| {
856+ cx.stop_propagation();
857+ })
858+ .on_any_mouse_down(|_, _, cx| {
859+ cx.stop_propagation();
860+ })
861+ .on_mouse_up(
862+ MouseButton::Left,
863+ cx.listener(|_, _, _, cx| {
864+ cx.stop_propagation();
865+ }),
866+ )
867+ .on_scroll_wheel(cx.listener(|_, _, _, cx| {
868+ cx.notify();
869+ }))
870+ .h_full()
871+ .absolute()
872+ .right_1()
873+ .top_1()
874+ .bottom_0()
875+ .w(px(12.))
876+ .cursor_default()
877+ .children(Scrollbar::vertical(self.scrollbar_state.clone()))
878+ }
879+}
880+
881+impl Focusable for VariableList {
882+ fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
883+ self.focus_handle.clone()
884+ }
885+}
886+
887+impl Render for VariableList {
888+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
889+ self.build_entries(cx);
890+
891+ v_flex()
892+ .key_context("VariableList")
893+ .id("variable-list")
894+ .group("variable-list")
895+ .overflow_y_scroll()
896+ .size_full()
897+ .track_focus(&self.focus_handle(cx))
898+ .on_action(cx.listener(Self::select_first))
899+ .on_action(cx.listener(Self::select_last))
900+ .on_action(cx.listener(Self::select_prev))
901+ .on_action(cx.listener(Self::select_next))
902+ .on_action(cx.listener(Self::expand_selected_entry))
903+ .on_action(cx.listener(Self::collapse_selected_entry))
904+ .on_action(cx.listener(Self::cancel_variable_edit))
905+ .on_action(cx.listener(Self::confirm_variable_edit))
906+ //
907+ .child(
908+ uniform_list(
909+ cx.entity().clone(),
910+ "variable-list",
911+ self.entries.len(),
912+ move |this, range, window, cx| this.render_entries(range, window, cx),
913+ )
914+ .track_scroll(self.list_handle.clone())
915+ .gap_1_5()
916+ .size_full()
917+ .flex_grow(),
918+ )
919+ .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
920+ deferred(
921+ anchored()
922+ .position(*position)
923+ .anchor(gpui::Corner::TopLeft)
924+ .child(menu.clone()),
925+ )
926+ .with_priority(1)
927+ }))
928+ .child(self.render_vertical_scrollbar(cx))
929+ }
930+}
931+
932+struct EntryColors {
933+ default: Hsla,
934+ hover: Hsla,
935+ marked_active: Hsla,
936+}
937+
938+fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
939+ let colors = cx.theme().colors();
940+
941+ EntryColors {
942+ default: colors.panel_background,
943+ hover: colors.ghost_element_hover,
944+ marked_active: colors.ghost_element_selected,
945+ }
946+}
crates/debugger_ui/src/session/starting.rs+80−0
@@ -0,0 +1,80 @@
1+use std::time::Duration;
2+
3+use anyhow::Result;
4+
5+use dap::client::SessionId;
6+use gpui::{
7+ percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task,
8+ Transformation,
9+};
10+use project::debugger::session::Session;
11+use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled};
12+
13+pub(crate) struct StartingState {
14+ focus_handle: FocusHandle,
15+ pub(super) session_id: SessionId,
16+ _notify_parent: Task<()>,
17+}
18+
19+pub(crate) enum StartingEvent {
20+ Failed,
21+ Finished(Entity<Session>),
22+}
23+
24+impl EventEmitter<StartingEvent> for StartingState {}
25+
26+impl StartingState {
27+ pub(crate) fn new(
28+ session_id: SessionId,
29+ task: Task<Result<Entity<Session>>>,
30+ cx: &mut Context<Self>,
31+ ) -> Self {
32+ let _notify_parent = cx.spawn(move |this, mut cx| async move {
33+ let entity = task.await;
34+
35+ this.update(&mut cx, |_, cx| {
36+ if let Ok(entity) = entity {
37+ cx.emit(StartingEvent::Finished(entity))
38+ } else {
39+ cx.emit(StartingEvent::Failed)
40+ }
41+ })
42+ .ok();
43+ });
44+ Self {
45+ session_id,
46+ focus_handle: cx.focus_handle(),
47+ _notify_parent,
48+ }
49+ }
50+}
51+
52+impl Focusable for StartingState {
53+ fn focus_handle(&self, _: &ui::App) -> FocusHandle {
54+ self.focus_handle.clone()
55+ }
56+}
57+
58+impl Render for StartingState {
59+ fn render(
60+ &mut self,
61+ _window: &mut ui::Window,
62+ _cx: &mut ui::Context<'_, Self>,
63+ ) -> impl ui::IntoElement {
64+ v_flex()
65+ .size_full()
66+ .gap_1()
67+ .items_center()
68+ .child("Starting a debug adapter")
69+ .child(
70+ Icon::new(IconName::ArrowCircle)
71+ .color(Color::Info)
72+ .with_animation(
73+ "arrow-circle",
74+ Animation::new(Duration::from_secs(2)).repeat(),
75+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
76+ )
77+ .into_any_element(),
78+ )
79+ }
80+}
crates/debugger_ui/src/tests.rs+75−0
@@ -0,0 +1,75 @@
1+use gpui::{Entity, TestAppContext, WindowHandle};
2+use project::Project;
3+use settings::SettingsStore;
4+use terminal_view::terminal_panel::TerminalPanel;
5+use workspace::Workspace;
6+
7+use crate::{debugger_panel::DebugPanel, session::DebugSession};
8+
9+mod attach_modal;
10+mod console;
11+mod debugger_panel;
12+mod module_list;
13+mod stack_frame_list;
14+mod variable_list;
15+
16+pub fn init_test(cx: &mut gpui::TestAppContext) {
17+ if std::env::var("RUST_LOG").is_ok() {
18+ env_logger::try_init().ok();
19+ }
20+
21+ cx.update(|cx| {
22+ let settings = SettingsStore::test(cx);
23+ cx.set_global(settings);
24+ terminal_view::init(cx);
25+ theme::init(theme::LoadThemes::JustBase, cx);
26+ command_palette_hooks::init(cx);
27+ language::init(cx);
28+ workspace::init_settings(cx);
29+ Project::init_settings(cx);
30+ editor::init(cx);
31+ crate::init(cx);
32+ });
33+}
34+
35+pub async fn init_test_workspace(
36+ project: &Entity<Project>,
37+ cx: &mut TestAppContext,
38+) -> WindowHandle<Workspace> {
39+ let workspace_handle =
40+ cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
41+
42+ let debugger_panel = workspace_handle
43+ .update(cx, |_, window, cx| cx.spawn_in(window, DebugPanel::load))
44+ .unwrap()
45+ .await
46+ .expect("Failed to load debug panel");
47+
48+ let terminal_panel = workspace_handle
49+ .update(cx, |_, window, cx| cx.spawn_in(window, TerminalPanel::load))
50+ .unwrap()
51+ .await
52+ .expect("Failed to load terminal panel");
53+
54+ workspace_handle
55+ .update(cx, |workspace, window, cx| {
56+ workspace.add_panel(debugger_panel, window, cx);
57+ workspace.add_panel(terminal_panel, window, cx);
58+ })
59+ .unwrap();
60+ workspace_handle
61+}
62+
63+pub fn active_debug_session_panel(
64+ workspace: WindowHandle<Workspace>,
65+ cx: &mut TestAppContext,
66+) -> Entity<DebugSession> {
67+ workspace
68+ .update(cx, |workspace, _window, cx| {
69+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
70+ debug_panel
71+ .update(cx, |this, cx| this.active_session(cx))
72+ .unwrap()
73+ })
74+ .unwrap()
75+}
crates/debugger_ui/src/tests/attach_modal.rs+136−0
@@ -0,0 +1,136 @@
1+use crate::*;
2+use attach_modal::AttachModal;
3+use dap::client::SessionId;
4+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
5+use menu::Confirm;
6+use project::{FakeFs, Project};
7+use serde_json::json;
8+use task::AttachConfig;
9+use tests::{init_test, init_test_workspace};
10+
11+#[gpui::test]
12+async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13+ init_test(cx);
14+
15+ let fs = FakeFs::new(executor.clone());
16+
17+ fs.insert_tree(
18+ "/project",
19+ json!({
20+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
21+ }),
22+ )
23+ .await;
24+
25+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
26+ let workspace = init_test_workspace(&project, cx).await;
27+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
28+
29+ let task = project.update(cx, |project, cx| {
30+ project.start_debug_session(
31+ dap::test_config(
32+ dap::DebugRequestType::Attach(AttachConfig {
33+ process_id: Some(10),
34+ }),
35+ None,
36+ None,
37+ ),
38+ cx,
39+ )
40+ });
41+
42+ let session = task.await.unwrap();
43+
44+ cx.run_until_parked();
45+
46+ // assert we didn't show the attach modal
47+ workspace
48+ .update(cx, |workspace, _window, cx| {
49+ assert!(workspace.active_modal::<AttachModal>(cx).is_none());
50+ })
51+ .unwrap();
52+
53+ let shutdown_session = project.update(cx, |project, cx| {
54+ project.dap_store().update(cx, |dap_store, cx| {
55+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
56+ })
57+ });
58+
59+ shutdown_session.await.unwrap();
60+}
61+
62+#[gpui::test]
63+async fn test_show_attach_modal_and_select_process(
64+ executor: BackgroundExecutor,
65+ cx: &mut TestAppContext,
66+) {
67+ init_test(cx);
68+
69+ let fs = FakeFs::new(executor.clone());
70+
71+ fs.insert_tree(
72+ "/project",
73+ json!({
74+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
75+ }),
76+ )
77+ .await;
78+
79+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
80+ let workspace = init_test_workspace(&project, cx).await;
81+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
82+
83+ let attach_modal = workspace
84+ .update(cx, |workspace, window, cx| {
85+ workspace.toggle_modal(window, cx, |window, cx| {
86+ AttachModal::new(
87+ project.clone(),
88+ dap::test_config(
89+ dap::DebugRequestType::Attach(AttachConfig { process_id: None }),
90+ None,
91+ None,
92+ ),
93+ window,
94+ cx,
95+ )
96+ });
97+
98+ workspace.active_modal::<AttachModal>(cx).unwrap()
99+ })
100+ .unwrap();
101+
102+ cx.run_until_parked();
103+
104+ // assert we got the expected processes
105+ workspace
106+ .update(cx, |_, _, cx| {
107+ let names =
108+ attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx));
109+
110+ // we filtered out all processes that are not the current process(zed itself)
111+ assert_eq!(1, names.len());
112+ })
113+ .unwrap();
114+
115+ // select the only existing process
116+ cx.dispatch_action(Confirm);
117+
118+ cx.run_until_parked();
119+
120+ // assert attach modal was dismissed
121+ workspace
122+ .update(cx, |workspace, _window, cx| {
123+ assert!(workspace.active_modal::<AttachModal>(cx).is_none());
124+ })
125+ .unwrap();
126+
127+ let shutdown_session = project.update(cx, |project, cx| {
128+ project.dap_store().update(cx, |dap_store, cx| {
129+ let session = dap_store.session_by_id(SessionId(0)).unwrap();
130+
131+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
132+ })
133+ });
134+
135+ shutdown_session.await.unwrap();
136+}
crates/debugger_ui/src/tests/console.rs+916−0
@@ -0,0 +1,916 @@
1+use crate::{tests::active_debug_session_panel, *};
2+use dap::requests::StackTrace;
3+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
4+use project::{FakeFs, Project};
5+use serde_json::json;
6+use tests::{init_test, init_test_workspace};
7+
8+#[gpui::test]
9+async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) {
10+ init_test(cx);
11+
12+ let fs = FakeFs::new(executor.clone());
13+
14+ fs.insert_tree(
15+ "/project",
16+ json!({
17+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
18+ }),
19+ )
20+ .await;
21+
22+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
23+ let workspace = init_test_workspace(&project, cx).await;
24+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
25+ workspace
26+ .update(cx, |workspace, window, cx| {
27+ workspace.focus_panel::<DebugPanel>(window, cx);
28+ })
29+ .unwrap();
30+
31+ let task = project.update(cx, |project, cx| {
32+ project.start_debug_session(
33+ dap::test_config(dap::DebugRequestType::Launch, None, None),
34+ cx,
35+ )
36+ });
37+
38+ let session = task.await.unwrap();
39+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
40+
41+ client
42+ .on_request::<StackTrace, _>(move |_, _| {
43+ Ok(dap::StackTraceResponse {
44+ stack_frames: Vec::default(),
45+ total_frames: None,
46+ })
47+ })
48+ .await;
49+
50+ client
51+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
52+ category: None,
53+ output: "First console output line before thread stopped!".to_string(),
54+ data: None,
55+ variables_reference: None,
56+ source: None,
57+ line: None,
58+ column: None,
59+ group: None,
60+ location_reference: None,
61+ }))
62+ .await;
63+
64+ client
65+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
66+ category: Some(dap::OutputEventCategory::Stdout),
67+ output: "First output line before thread stopped!".to_string(),
68+ data: None,
69+ variables_reference: None,
70+ source: None,
71+ line: None,
72+ column: None,
73+ group: None,
74+ location_reference: None,
75+ }))
76+ .await;
77+
78+ client
79+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
80+ reason: dap::StoppedEventReason::Pause,
81+ description: None,
82+ thread_id: Some(1),
83+ preserve_focus_hint: None,
84+ text: None,
85+ all_threads_stopped: None,
86+ hit_breakpoint_ids: None,
87+ }))
88+ .await;
89+
90+ cx.run_until_parked();
91+
92+ let running_state =
93+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
94+ cx.focus_self(window);
95+ item.mode()
96+ .as_running()
97+ .expect("Session should be running by this point")
98+ .clone()
99+ });
100+
101+ running_state.update(cx, |state, cx| {
102+ state.set_thread_item(session::ThreadItem::Console, cx);
103+ cx.refresh_windows();
104+ });
105+ cx.run_until_parked();
106+
107+ // assert we have output from before the thread stopped
108+ workspace
109+ .update(cx, |workspace, _window, cx| {
110+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
111+ let active_debug_session_panel = debug_panel
112+ .update(cx, |this, cx| this.active_session(cx))
113+ .unwrap();
114+
115+ assert_eq!(
116+ "First console output line before thread stopped!\nFirst output line before thread stopped!\n",
117+ active_debug_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
118+ );
119+ })
120+ .unwrap();
121+
122+ client
123+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
124+ category: Some(dap::OutputEventCategory::Stdout),
125+ output: "Second output line after thread stopped!".to_string(),
126+ data: None,
127+ variables_reference: None,
128+ source: None,
129+ line: None,
130+ column: None,
131+ group: None,
132+ location_reference: None,
133+ }))
134+ .await;
135+
136+ client
137+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
138+ category: Some(dap::OutputEventCategory::Console),
139+ output: "Second console output line after thread stopped!".to_string(),
140+ data: None,
141+ variables_reference: None,
142+ source: None,
143+ line: None,
144+ column: None,
145+ group: None,
146+ location_reference: None,
147+ }))
148+ .await;
149+
150+ cx.run_until_parked();
151+ running_state.update(cx, |state, cx| {
152+ state.set_thread_item(session::ThreadItem::Console, cx);
153+ cx.refresh_windows();
154+ });
155+ cx.run_until_parked();
156+
157+ // assert we have output from before and after the thread stopped
158+ workspace
159+ .update(cx, |workspace, _window, cx| {
160+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
161+ let active_session_panel = debug_panel
162+ .update(cx, |this, cx| this.active_session(cx))
163+ .unwrap();
164+
165+ assert_eq!(
166+ "First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
167+ active_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
168+ );
169+ })
170+ .unwrap();
171+
172+ let shutdown_session = project.update(cx, |project, cx| {
173+ project.dap_store().update(cx, |dap_store, cx| {
174+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
175+ })
176+ });
177+
178+ shutdown_session.await.unwrap();
179+}
180+
181+// #[gpui::test]
182+// async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) {
183+// init_test(cx);
184+
185+// let fs = FakeFs::new(executor.clone());
186+
187+// fs.insert_tree(
188+// "/project",
189+// json!({
190+// "main.rs": "First line\nSecond line\nThird line\nFourth line",
191+// }),
192+// )
193+// .await;
194+
195+// let project = Project::test(fs, ["/project".as_ref()], cx).await;
196+// let workspace = init_test_workspace(&project, cx).await;
197+// let cx = &mut VisualTestContext::from_window(*workspace, cx);
198+
199+// let task = project.update(cx, |project, cx| {
200+// project.start_debug_session(
201+// dap::test_config(dap::DebugRequestType::Launch, None, None),
202+// cx,
203+// )
204+// });
205+
206+// let session = task.await.unwrap();
207+// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
208+
209+// client
210+// .on_request::<StackTrace, _>(move |_, _| {
211+// Ok(dap::StackTraceResponse {
212+// stack_frames: Vec::default(),
213+// total_frames: None,
214+// })
215+// })
216+// .await;
217+
218+// client
219+// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
220+// reason: dap::StoppedEventReason::Pause,
221+// description: None,
222+// thread_id: Some(1),
223+// preserve_focus_hint: None,
224+// text: None,
225+// all_threads_stopped: None,
226+// hit_breakpoint_ids: None,
227+// }))
228+// .await;
229+
230+// client
231+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
232+// category: None,
233+// output: "First line".to_string(),
234+// data: None,
235+// variables_reference: None,
236+// source: None,
237+// line: None,
238+// column: None,
239+// group: None,
240+// location_reference: None,
241+// }))
242+// .await;
243+
244+// client
245+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
246+// category: Some(dap::OutputEventCategory::Stdout),
247+// output: "First group".to_string(),
248+// data: None,
249+// variables_reference: None,
250+// source: None,
251+// line: None,
252+// column: None,
253+// group: Some(dap::OutputEventGroup::Start),
254+// location_reference: None,
255+// }))
256+// .await;
257+
258+// client
259+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
260+// category: Some(dap::OutputEventCategory::Stdout),
261+// output: "First item in group 1".to_string(),
262+// data: None,
263+// variables_reference: None,
264+// source: None,
265+// line: None,
266+// column: None,
267+// group: None,
268+// location_reference: None,
269+// }))
270+// .await;
271+
272+// client
273+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
274+// category: Some(dap::OutputEventCategory::Stdout),
275+// output: "Second item in group 1".to_string(),
276+// data: None,
277+// variables_reference: None,
278+// source: None,
279+// line: None,
280+// column: None,
281+// group: None,
282+// location_reference: None,
283+// }))
284+// .await;
285+
286+// client
287+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
288+// category: Some(dap::OutputEventCategory::Stdout),
289+// output: "Second group".to_string(),
290+// data: None,
291+// variables_reference: None,
292+// source: None,
293+// line: None,
294+// column: None,
295+// group: Some(dap::OutputEventGroup::Start),
296+// location_reference: None,
297+// }))
298+// .await;
299+
300+// client
301+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
302+// category: Some(dap::OutputEventCategory::Stdout),
303+// output: "First item in group 2".to_string(),
304+// data: None,
305+// variables_reference: None,
306+// source: None,
307+// line: None,
308+// column: None,
309+// group: None,
310+// location_reference: None,
311+// }))
312+// .await;
313+
314+// client
315+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
316+// category: Some(dap::OutputEventCategory::Stdout),
317+// output: "Second item in group 2".to_string(),
318+// data: None,
319+// variables_reference: None,
320+// source: None,
321+// line: None,
322+// column: None,
323+// group: None,
324+// location_reference: None,
325+// }))
326+// .await;
327+
328+// client
329+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
330+// category: Some(dap::OutputEventCategory::Stdout),
331+// output: "End group 2".to_string(),
332+// data: None,
333+// variables_reference: None,
334+// source: None,
335+// line: None,
336+// column: None,
337+// group: Some(dap::OutputEventGroup::End),
338+// location_reference: None,
339+// }))
340+// .await;
341+
342+// client
343+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
344+// category: Some(dap::OutputEventCategory::Stdout),
345+// output: "Third group".to_string(),
346+// data: None,
347+// variables_reference: None,
348+// source: None,
349+// line: None,
350+// column: None,
351+// group: Some(dap::OutputEventGroup::StartCollapsed),
352+// location_reference: None,
353+// }))
354+// .await;
355+
356+// client
357+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
358+// category: Some(dap::OutputEventCategory::Stdout),
359+// output: "First item in group 3".to_string(),
360+// data: None,
361+// variables_reference: None,
362+// source: None,
363+// line: None,
364+// column: None,
365+// group: None,
366+// location_reference: None,
367+// }))
368+// .await;
369+
370+// client
371+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
372+// category: Some(dap::OutputEventCategory::Stdout),
373+// output: "Second item in group 3".to_string(),
374+// data: None,
375+// variables_reference: None,
376+// source: None,
377+// line: None,
378+// column: None,
379+// group: None,
380+// location_reference: None,
381+// }))
382+// .await;
383+
384+// client
385+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
386+// category: Some(dap::OutputEventCategory::Stdout),
387+// output: "End group 3".to_string(),
388+// data: None,
389+// variables_reference: None,
390+// source: None,
391+// line: None,
392+// column: None,
393+// group: Some(dap::OutputEventGroup::End),
394+// location_reference: None,
395+// }))
396+// .await;
397+
398+// client
399+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
400+// category: Some(dap::OutputEventCategory::Stdout),
401+// output: "Third item in group 1".to_string(),
402+// data: None,
403+// variables_reference: None,
404+// source: None,
405+// line: None,
406+// column: None,
407+// group: None,
408+// location_reference: None,
409+// }))
410+// .await;
411+
412+// client
413+// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
414+// category: Some(dap::OutputEventCategory::Stdout),
415+// output: "Second item".to_string(),
416+// data: None,
417+// variables_reference: None,
418+// source: None,
419+// line: None,
420+// column: None,
421+// group: Some(dap::OutputEventGroup::End),
422+// location_reference: None,
423+// }))
424+// .await;
425+
426+// cx.run_until_parked();
427+
428+// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
429+// debug_panel_item
430+// .mode()
431+// .as_running()
432+// .unwrap()
433+// .update(cx, |running_state, cx| {
434+// running_state.console().update(cx, |console, cx| {
435+// console.editor().update(cx, |editor, cx| {
436+// pretty_assertions::assert_eq!(
437+// "
438+// First line
439+// First group
440+// First item in group 1
441+// Second item in group 1
442+// Second group
443+// First item in group 2
444+// Second item in group 2
445+// End group 2
446+// ⋯ End group 3
447+// Third item in group 1
448+// Second item
449+// "
450+// .unindent(),
451+// editor.display_text(cx)
452+// );
453+// })
454+// });
455+// });
456+// });
457+
458+// let shutdown_session = project.update(cx, |project, cx| {
459+// project.dap_store().update(cx, |dap_store, cx| {
460+// dap_store.shutdown_session(session.read(cx).session_id(), cx)
461+// })
462+// });
463+
464+// shutdown_session.await.unwrap();
465+// }
466+
467+// todo(debugger): enable this again
468+// #[gpui::test]
469+// async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) {
470+// init_test(cx);
471+
472+// const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}";
473+
474+// let called_evaluate = Arc::new(AtomicBool::new(false));
475+
476+// let fs = FakeFs::new(executor.clone());
477+
478+// let test_file_content = r#"
479+// const variable1 = {
480+// nested1: "Nested 1",
481+// nested2: "Nested 2",
482+// };
483+// const variable2 = "Value 2";
484+// const variable3 = "Value 3";
485+// "#
486+// .unindent();
487+
488+// fs.insert_tree(
489+// "/project",
490+// json!({
491+// "src": {
492+// "test.js": test_file_content,
493+// }
494+// }),
495+// )
496+// .await;
497+
498+// let project = Project::test(fs, ["/project".as_ref()], cx).await;
499+// let workspace = init_test_workspace(&project, cx).await;
500+// let cx = &mut VisualTestContext::from_window(*workspace, cx);
501+
502+// let task = project.update(cx, |project, cx| {
503+// project.start_debug_session(dap::test_config(None), cx)
504+// });
505+
506+// let session = task.await.unwrap();
507+// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
508+
509+// client
510+// .on_request::<Threads, _>(move |_, _| {
511+// Ok(dap::ThreadsResponse {
512+// threads: vec![dap::Thread {
513+// id: 1,
514+// name: "Thread 1".into(),
515+// }],
516+// })
517+// })
518+// .await;
519+
520+// let stack_frames = vec![StackFrame {
521+// id: 1,
522+// name: "Stack Frame 1".into(),
523+// source: Some(dap::Source {
524+// name: Some("test.js".into()),
525+// path: Some("/project/src/test.js".into()),
526+// source_reference: None,
527+// presentation_hint: None,
528+// origin: None,
529+// sources: None,
530+// adapter_data: None,
531+// checksums: None,
532+// }),
533+// line: 3,
534+// column: 1,
535+// end_line: None,
536+// end_column: None,
537+// can_restart: None,
538+// instruction_pointer_reference: None,
539+// module_id: None,
540+// presentation_hint: None,
541+// }];
542+
543+// client
544+// .on_request::<StackTrace, _>({
545+// let stack_frames = Arc::new(stack_frames.clone());
546+// move |_, args| {
547+// assert_eq!(1, args.thread_id);
548+
549+// Ok(dap::StackTraceResponse {
550+// stack_frames: (*stack_frames).clone(),
551+// total_frames: None,
552+// })
553+// }
554+// })
555+// .await;
556+
557+// let scopes = vec![
558+// Scope {
559+// name: "Scope 1".into(),
560+// presentation_hint: None,
561+// variables_reference: 2,
562+// named_variables: None,
563+// indexed_variables: None,
564+// expensive: false,
565+// source: None,
566+// line: None,
567+// column: None,
568+// end_line: None,
569+// end_column: None,
570+// },
571+// Scope {
572+// name: "Scope 2".into(),
573+// presentation_hint: None,
574+// variables_reference: 4,
575+// named_variables: None,
576+// indexed_variables: None,
577+// expensive: false,
578+// source: None,
579+// line: None,
580+// column: None,
581+// end_line: None,
582+// end_column: None,
583+// },
584+// ];
585+
586+// client
587+// .on_request::<Scopes, _>({
588+// let scopes = Arc::new(scopes.clone());
589+// move |_, args| {
590+// assert_eq!(1, args.frame_id);
591+
592+// Ok(dap::ScopesResponse {
593+// scopes: (*scopes).clone(),
594+// })
595+// }
596+// })
597+// .await;
598+
599+// let scope1_variables = Arc::new(Mutex::new(vec![
600+// Variable {
601+// name: "variable1".into(),
602+// value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(),
603+// type_: None,
604+// presentation_hint: None,
605+// evaluate_name: None,
606+// variables_reference: 3,
607+// named_variables: None,
608+// indexed_variables: None,
609+// memory_reference: None,
610+// declaration_location_reference: None,
611+// value_location_reference: None,
612+// },
613+// Variable {
614+// name: "variable2".into(),
615+// value: "Value 2".into(),
616+// type_: None,
617+// presentation_hint: None,
618+// evaluate_name: None,
619+// variables_reference: 0,
620+// named_variables: None,
621+// indexed_variables: None,
622+// memory_reference: None,
623+// declaration_location_reference: None,
624+// value_location_reference: None,
625+// },
626+// ]));
627+
628+// let nested_variables = vec![
629+// Variable {
630+// name: "nested1".into(),
631+// value: "Nested 1".into(),
632+// type_: None,
633+// presentation_hint: None,
634+// evaluate_name: None,
635+// variables_reference: 0,
636+// named_variables: None,
637+// indexed_variables: None,
638+// memory_reference: None,
639+// declaration_location_reference: None,
640+// value_location_reference: None,
641+// },
642+// Variable {
643+// name: "nested2".into(),
644+// value: "Nested 2".into(),
645+// type_: None,
646+// presentation_hint: None,
647+// evaluate_name: None,
648+// variables_reference: 0,
649+// named_variables: None,
650+// indexed_variables: None,
651+// memory_reference: None,
652+// declaration_location_reference: None,
653+// value_location_reference: None,
654+// },
655+// ];
656+
657+// let scope2_variables = vec![Variable {
658+// name: "variable3".into(),
659+// value: "Value 3".into(),
660+// type_: None,
661+// presentation_hint: None,
662+// evaluate_name: None,
663+// variables_reference: 0,
664+// named_variables: None,
665+// indexed_variables: None,
666+// memory_reference: None,
667+// declaration_location_reference: None,
668+// value_location_reference: None,
669+// }];
670+
671+// client
672+// .on_request::<Variables, _>({
673+// let scope1_variables = scope1_variables.clone();
674+// let nested_variables = Arc::new(nested_variables.clone());
675+// let scope2_variables = Arc::new(scope2_variables.clone());
676+// move |_, args| match args.variables_reference {
677+// 4 => Ok(dap::VariablesResponse {
678+// variables: (*scope2_variables).clone(),
679+// }),
680+// 3 => Ok(dap::VariablesResponse {
681+// variables: (*nested_variables).clone(),
682+// }),
683+// 2 => Ok(dap::VariablesResponse {
684+// variables: scope1_variables.lock().unwrap().clone(),
685+// }),
686+// id => unreachable!("unexpected variables reference {id}"),
687+// }
688+// })
689+// .await;
690+
691+// client
692+// .on_request::<Evaluate, _>({
693+// let called_evaluate = called_evaluate.clone();
694+// let scope1_variables = scope1_variables.clone();
695+// move |_, args| {
696+// called_evaluate.store(true, Ordering::SeqCst);
697+
698+// assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression);
699+// assert_eq!(Some(1), args.frame_id);
700+// assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context);
701+
702+// scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string();
703+
704+// Ok(dap::EvaluateResponse {
705+// result: NEW_VALUE.into(),
706+// type_: None,
707+// presentation_hint: None,
708+// variables_reference: 0,
709+// named_variables: None,
710+// indexed_variables: None,
711+// memory_reference: None,
712+// value_location_reference: None,
713+// })
714+// }
715+// })
716+// .await;
717+
718+// client
719+// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
720+// reason: dap::StoppedEventReason::Pause,
721+// description: None,
722+// thread_id: Some(1),
723+// preserve_focus_hint: None,
724+// text: None,
725+// all_threads_stopped: None,
726+// hit_breakpoint_ids: None,
727+// }))
728+// .await;
729+
730+// cx.run_until_parked();
731+
732+// // toggle nested variables for scope 1
733+// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
734+// debug_panel_item
735+// .mode()
736+// .as_running()
737+// .unwrap()
738+// .update(cx, |running_state, cx| {
739+// running_state
740+// .variable_list()
741+// .update(cx, |variable_list, cx| {
742+// variable_list.toggle_variable(
743+// &VariablePath {
744+// indices: Arc::from([scopes[0].variables_reference]),
745+// },
746+// cx,
747+// );
748+// });
749+// });
750+// });
751+
752+// cx.run_until_parked();
753+
754+// active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
755+// debug_panel_item
756+// .mode()
757+// .as_running()
758+// .unwrap()
759+// .update(cx, |running_state, cx| {
760+// running_state.console().update(cx, |console, item_cx| {
761+// console
762+// .query_bar()
763+// .update(item_cx, |query_bar, console_cx| {
764+// query_bar.set_text(
765+// format!("$variable1 = {}", NEW_VALUE),
766+// window,
767+// console_cx,
768+// );
769+// });
770+
771+// console.evaluate(&menu::Confirm, window, item_cx);
772+// });
773+// });
774+// });
775+
776+// cx.run_until_parked();
777+
778+// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
779+// assert_eq!(
780+// "",
781+// debug_panel_item
782+// .mode()
783+// .as_running()
784+// .unwrap()
785+// .read(cx)
786+// .console()
787+// .read(cx)
788+// .query_bar()
789+// .read(cx)
790+// .text(cx)
791+// .as_str()
792+// );
793+
794+// assert_eq!(
795+// format!("{}\n", NEW_VALUE),
796+// debug_panel_item
797+// .mode()
798+// .as_running()
799+// .unwrap()
800+// .read(cx)
801+// .console()
802+// .read(cx)
803+// .editor()
804+// .read(cx)
805+// .text(cx)
806+// .as_str()
807+// );
808+
809+// debug_panel_item
810+// .mode()
811+// .as_running()
812+// .unwrap()
813+// .update(cx, |running_state, cx| {
814+// running_state
815+// .variable_list()
816+// .update(cx, |variable_list, _| {
817+// let scope1_variables = scope1_variables.lock().unwrap().clone();
818+
819+// // scope 1
820+// // assert_eq!(
821+// // vec![
822+// // VariableContainer {
823+// // container_reference: scopes[0].variables_reference,
824+// // variable: scope1_variables[0].clone(),
825+// // depth: 1,
826+// // },
827+// // VariableContainer {
828+// // container_reference: scope1_variables[0].variables_reference,
829+// // variable: nested_variables[0].clone(),
830+// // depth: 2,
831+// // },
832+// // VariableContainer {
833+// // container_reference: scope1_variables[0].variables_reference,
834+// // variable: nested_variables[1].clone(),
835+// // depth: 2,
836+// // },
837+// // VariableContainer {
838+// // container_reference: scopes[0].variables_reference,
839+// // variable: scope1_variables[1].clone(),
840+// // depth: 1,
841+// // },
842+// // ],
843+// // variable_list.variables_by_scope(1, 2).unwrap().variables()
844+// // );
845+
846+// // scope 2
847+// // assert_eq!(
848+// // vec![VariableContainer {
849+// // container_reference: scopes[1].variables_reference,
850+// // variable: scope2_variables[0].clone(),
851+// // depth: 1,
852+// // }],
853+// // variable_list.variables_by_scope(1, 4).unwrap().variables()
854+// // );
855+
856+// variable_list.assert_visual_entries(vec![
857+// "v Scope 1",
858+// " v variable1",
859+// " > nested1",
860+// " > nested2",
861+// " > variable2",
862+// ]);
863+
864+// // assert visual entries
865+// // assert_eq!(
866+// // vec![
867+// // VariableListEntry::Scope(scopes[0].clone()),
868+// // VariableListEntry::Variable {
869+// // depth: 1,
870+// // scope: Arc::new(scopes[0].clone()),
871+// // has_children: true,
872+// // variable: Arc::new(scope1_variables[0].clone()),
873+// // container_reference: scopes[0].variables_reference,
874+// // },
875+// // VariableListEntry::Variable {
876+// // depth: 2,
877+// // scope: Arc::new(scopes[0].clone()),
878+// // has_children: false,
879+// // variable: Arc::new(nested_variables[0].clone()),
880+// // container_reference: scope1_variables[0].variables_reference,
881+// // },
882+// // VariableListEntry::Variable {
883+// // depth: 2,
884+// // scope: Arc::new(scopes[0].clone()),
885+// // has_children: false,
886+// // variable: Arc::new(nested_variables[1].clone()),
887+// // container_reference: scope1_variables[0].variables_reference,
888+// // },
889+// // VariableListEntry::Variable {
890+// // depth: 1,
891+// // scope: Arc::new(scopes[0].clone()),
892+// // has_children: false,
893+// // variable: Arc::new(scope1_variables[1].clone()),
894+// // container_reference: scopes[0].variables_reference,
895+// // },
896+// // VariableListEntry::Scope(scopes[1].clone()),
897+// // ],
898+// // variable_list.entries().get(&1).unwrap().clone()
899+// // );
900+// });
901+// });
902+// });
903+
904+// assert!(
905+// called_evaluate.load(std::sync::atomic::Ordering::SeqCst),
906+// "Expected evaluate request to be called"
907+// );
908+
909+// let shutdown_session = project.update(cx, |project, cx| {
910+// project.dap_store().update(cx, |dap_store, cx| {
911+// dap_store.shutdown_session(&session.read(cx).session_id(), cx)
912+// })
913+// });
914+
915+// shutdown_session.await.unwrap();
916+// }
crates/debugger_ui/src/tests/debugger_panel.rs+1084−0
@@ -0,0 +1,1084 @@
1+use crate::*;
2+use dap::{
3+ client::SessionId,
4+ requests::{
5+ Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
6+ StartDebugging, StepBack, StepIn, StepOut, Threads,
7+ },
8+ DebugRequestType, ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint,
9+ StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
10+};
11+use editor::{
12+ actions::{self},
13+ Editor, EditorMode, MultiBuffer,
14+};
15+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
16+use project::{
17+ debugger::session::{ThreadId, ThreadStatus},
18+ FakeFs, Project,
19+};
20+use serde_json::json;
21+use std::{
22+ path::Path,
23+ sync::{
24+ atomic::{AtomicBool, Ordering},
25+ Arc,
26+ },
27+};
28+use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
29+use tests::{active_debug_session_panel, init_test, init_test_workspace};
30+use util::path;
31+use workspace::{dock::Panel, Item};
32+
33+#[gpui::test]
34+async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
35+ init_test(cx);
36+
37+ let fs = FakeFs::new(executor.clone());
38+
39+ fs.insert_tree(
40+ "/project",
41+ json!({
42+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
43+ }),
44+ )
45+ .await;
46+
47+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
48+ let workspace = init_test_workspace(&project, cx).await;
49+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
50+
51+ let task = project.update(cx, |project, cx| {
52+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
53+ });
54+
55+ let session = task.await.unwrap();
56+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
57+
58+ client
59+ .on_request::<Threads, _>(move |_, _| {
60+ Ok(dap::ThreadsResponse {
61+ threads: vec![dap::Thread {
62+ id: 1,
63+ name: "Thread 1".into(),
64+ }],
65+ })
66+ })
67+ .await;
68+
69+ client
70+ .on_request::<StackTrace, _>(move |_, _| {
71+ Ok(dap::StackTraceResponse {
72+ stack_frames: Vec::default(),
73+ total_frames: None,
74+ })
75+ })
76+ .await;
77+
78+ // assert we have a debug panel item before the session has stopped
79+ workspace
80+ .update(cx, |workspace, _window, cx| {
81+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
82+ let active_session = debug_panel.update(cx, |debug_panel, cx| {
83+ debug_panel.active_session(cx).unwrap()
84+ });
85+
86+ let running_state = active_session.update(cx, |active_session, _| {
87+ active_session
88+ .mode()
89+ .as_running()
90+ .expect("Session should be running by this point")
91+ .clone()
92+ });
93+
94+ debug_panel.update(cx, |this, cx| {
95+ assert!(this.active_session(cx).is_some());
96+ // we have one active session and one inert item
97+ assert_eq!(2, this.pane().unwrap().read(cx).items_len());
98+ assert!(running_state.read(cx).selected_thread_id().is_none());
99+ });
100+ })
101+ .unwrap();
102+
103+ client
104+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
105+ reason: dap::StoppedEventReason::Pause,
106+ description: None,
107+ thread_id: Some(1),
108+ preserve_focus_hint: None,
109+ text: None,
110+ all_threads_stopped: None,
111+ hit_breakpoint_ids: None,
112+ }))
113+ .await;
114+
115+ cx.run_until_parked();
116+
117+ workspace
118+ .update(cx, |workspace, _window, cx| {
119+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
120+ let active_session = debug_panel
121+ .update(cx, |this, cx| this.active_session(cx))
122+ .unwrap();
123+
124+ let running_state = active_session.update(cx, |active_session, _| {
125+ active_session
126+ .mode()
127+ .as_running()
128+ .expect("Session should be running by this point")
129+ .clone()
130+ });
131+
132+ // we have one active session and one inert item
133+ assert_eq!(
134+ 2,
135+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
136+ );
137+ assert_eq!(client.id(), running_state.read(cx).session_id());
138+ assert_eq!(
139+ ThreadId(1),
140+ running_state.read(cx).selected_thread_id().unwrap()
141+ );
142+ })
143+ .unwrap();
144+
145+ let shutdown_session = project.update(cx, |project, cx| {
146+ project.dap_store().update(cx, |dap_store, cx| {
147+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
148+ })
149+ });
150+
151+ shutdown_session.await.unwrap();
152+
153+ // assert we still have a debug panel item after the client shutdown
154+ workspace
155+ .update(cx, |workspace, _window, cx| {
156+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
157+
158+ let active_session = debug_panel
159+ .update(cx, |this, cx| this.active_session(cx))
160+ .unwrap();
161+
162+ let running_state = active_session.update(cx, |active_session, _| {
163+ active_session
164+ .mode()
165+ .as_running()
166+ .expect("Session should be running by this point")
167+ .clone()
168+ });
169+
170+ debug_panel.update(cx, |this, cx| {
171+ assert!(this.active_session(cx).is_some());
172+ assert_eq!(2, this.pane().unwrap().read(cx).items_len());
173+ assert_eq!(
174+ ThreadId(1),
175+ running_state.read(cx).selected_thread_id().unwrap()
176+ );
177+ });
178+ })
179+ .unwrap();
180+}
181+
182+#[gpui::test]
183+async fn test_we_can_only_have_one_panel_per_debug_session(
184+ executor: BackgroundExecutor,
185+ cx: &mut TestAppContext,
186+) {
187+ init_test(cx);
188+
189+ let fs = FakeFs::new(executor.clone());
190+
191+ fs.insert_tree(
192+ "/project",
193+ json!({
194+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
195+ }),
196+ )
197+ .await;
198+
199+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
200+ let workspace = init_test_workspace(&project, cx).await;
201+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
202+
203+ let task = project.update(cx, |project, cx| {
204+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
205+ });
206+
207+ let session = task.await.unwrap();
208+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
209+
210+ client
211+ .on_request::<Threads, _>(move |_, _| {
212+ Ok(dap::ThreadsResponse {
213+ threads: vec![dap::Thread {
214+ id: 1,
215+ name: "Thread 1".into(),
216+ }],
217+ })
218+ })
219+ .await;
220+
221+ client
222+ .on_request::<StackTrace, _>(move |_, _| {
223+ Ok(dap::StackTraceResponse {
224+ stack_frames: Vec::default(),
225+ total_frames: None,
226+ })
227+ })
228+ .await;
229+
230+ // assert we have a debug panel item before the session has stopped
231+ workspace
232+ .update(cx, |workspace, _window, cx| {
233+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
234+
235+ debug_panel.update(cx, |this, cx| {
236+ assert!(this.active_session(cx).is_some());
237+ // we have one active session and one inert item
238+ assert_eq!(2, this.pane().unwrap().read(cx).items_len());
239+ });
240+ })
241+ .unwrap();
242+
243+ client
244+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
245+ reason: dap::StoppedEventReason::Pause,
246+ description: None,
247+ thread_id: Some(1),
248+ preserve_focus_hint: None,
249+ text: None,
250+ all_threads_stopped: None,
251+ hit_breakpoint_ids: None,
252+ }))
253+ .await;
254+
255+ cx.run_until_parked();
256+
257+ // assert we added a debug panel item
258+ workspace
259+ .update(cx, |workspace, _window, cx| {
260+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
261+ let active_session = debug_panel
262+ .update(cx, |this, cx| this.active_session(cx))
263+ .unwrap();
264+
265+ let running_state = active_session.update(cx, |active_session, _| {
266+ active_session
267+ .mode()
268+ .as_running()
269+ .expect("Session should be running by this point")
270+ .clone()
271+ });
272+
273+ // we have one active session and one inert item
274+ assert_eq!(
275+ 2,
276+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
277+ );
278+ assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
279+ assert_eq!(
280+ ThreadId(1),
281+ running_state.read(cx).selected_thread_id().unwrap()
282+ );
283+ })
284+ .unwrap();
285+
286+ client
287+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
288+ reason: dap::StoppedEventReason::Pause,
289+ description: None,
290+ thread_id: Some(2),
291+ preserve_focus_hint: None,
292+ text: None,
293+ all_threads_stopped: None,
294+ hit_breakpoint_ids: None,
295+ }))
296+ .await;
297+
298+ cx.run_until_parked();
299+
300+ workspace
301+ .update(cx, |workspace, _window, cx| {
302+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
303+ let active_session = debug_panel
304+ .update(cx, |this, cx| this.active_session(cx))
305+ .unwrap();
306+
307+ let running_state = active_session.update(cx, |active_session, _| {
308+ active_session
309+ .mode()
310+ .as_running()
311+ .expect("Session should be running by this point")
312+ .clone()
313+ });
314+
315+ // we have one active session and one inert item
316+ assert_eq!(
317+ 2,
318+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
319+ );
320+ assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
321+ assert_eq!(
322+ ThreadId(1),
323+ running_state.read(cx).selected_thread_id().unwrap()
324+ );
325+ })
326+ .unwrap();
327+
328+ let shutdown_session = project.update(cx, |project, cx| {
329+ project.dap_store().update(cx, |dap_store, cx| {
330+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
331+ })
332+ });
333+
334+ shutdown_session.await.unwrap();
335+
336+ // assert we still have a debug panel item after the client shutdown
337+ workspace
338+ .update(cx, |workspace, _window, cx| {
339+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
340+ let active_session = debug_panel
341+ .update(cx, |this, cx| this.active_session(cx))
342+ .unwrap();
343+
344+ let running_state = active_session.update(cx, |active_session, _| {
345+ active_session
346+ .mode()
347+ .as_running()
348+ .expect("Session should be running by this point")
349+ .clone()
350+ });
351+
352+ debug_panel.update(cx, |this, cx| {
353+ assert!(this.active_session(cx).is_some());
354+ assert_eq!(2, this.pane().unwrap().read(cx).items_len());
355+ assert_eq!(
356+ ThreadId(1),
357+ running_state.read(cx).selected_thread_id().unwrap()
358+ );
359+ });
360+ })
361+ .unwrap();
362+}
363+
364+#[gpui::test]
365+async fn test_handle_successful_run_in_terminal_reverse_request(
366+ executor: BackgroundExecutor,
367+ cx: &mut TestAppContext,
368+) {
369+ init_test(cx);
370+
371+ let send_response = Arc::new(AtomicBool::new(false));
372+
373+ let fs = FakeFs::new(executor.clone());
374+
375+ fs.insert_tree(
376+ "/project",
377+ json!({
378+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
379+ }),
380+ )
381+ .await;
382+
383+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
384+ let workspace = init_test_workspace(&project, cx).await;
385+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
386+
387+ let task = project.update(cx, |project, cx| {
388+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
389+ });
390+
391+ let session = task.await.unwrap();
392+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
393+
394+ client
395+ .on_response::<RunInTerminal, _>({
396+ let send_response = send_response.clone();
397+ move |response| {
398+ send_response.store(true, Ordering::SeqCst);
399+
400+ assert!(response.success);
401+ assert!(response.body.is_some());
402+ }
403+ })
404+ .await;
405+
406+ client
407+ .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
408+ kind: None,
409+ title: None,
410+ cwd: std::env::temp_dir().to_string_lossy().to_string(),
411+ args: vec![],
412+ env: None,
413+ args_can_be_interpreted_by_shell: None,
414+ })
415+ .await;
416+
417+ cx.run_until_parked();
418+
419+ assert!(
420+ send_response.load(std::sync::atomic::Ordering::SeqCst),
421+ "Expected to receive response from reverse request"
422+ );
423+
424+ workspace
425+ .update(cx, |workspace, _window, cx| {
426+ let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
427+
428+ let panel = terminal_panel.read(cx).pane().unwrap().read(cx);
429+
430+ assert_eq!(1, panel.items_len());
431+ assert!(panel
432+ .active_item()
433+ .unwrap()
434+ .downcast::<TerminalView>()
435+ .unwrap()
436+ .read(cx)
437+ .terminal()
438+ .read(cx)
439+ .debug_terminal());
440+ })
441+ .unwrap();
442+
443+ let shutdown_session = project.update(cx, |project, cx| {
444+ project.dap_store().update(cx, |dap_store, cx| {
445+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
446+ })
447+ });
448+
449+ shutdown_session.await.unwrap();
450+}
451+
452+// // covers that we always send a response back, if something when wrong,
453+// // while spawning the terminal
454+#[gpui::test]
455+async fn test_handle_error_run_in_terminal_reverse_request(
456+ executor: BackgroundExecutor,
457+ cx: &mut TestAppContext,
458+) {
459+ init_test(cx);
460+
461+ let send_response = Arc::new(AtomicBool::new(false));
462+
463+ let fs = FakeFs::new(executor.clone());
464+
465+ fs.insert_tree(
466+ "/project",
467+ json!({
468+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
469+ }),
470+ )
471+ .await;
472+
473+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
474+ let workspace = init_test_workspace(&project, cx).await;
475+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
476+
477+ let task = project.update(cx, |project, cx| {
478+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
479+ });
480+
481+ let session = task.await.unwrap();
482+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
483+
484+ client
485+ .on_response::<RunInTerminal, _>({
486+ let send_response = send_response.clone();
487+ move |response| {
488+ send_response.store(true, Ordering::SeqCst);
489+
490+ assert!(!response.success);
491+ assert!(response.body.is_some());
492+ }
493+ })
494+ .await;
495+
496+ client
497+ .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
498+ kind: None,
499+ title: None,
500+ cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
501+ args: vec![],
502+ env: None,
503+ args_can_be_interpreted_by_shell: None,
504+ })
505+ .await;
506+
507+ cx.run_until_parked();
508+
509+ assert!(
510+ send_response.load(std::sync::atomic::Ordering::SeqCst),
511+ "Expected to receive response from reverse request"
512+ );
513+
514+ workspace
515+ .update(cx, |workspace, _window, cx| {
516+ let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
517+
518+ assert_eq!(
519+ 0,
520+ terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
521+ );
522+ })
523+ .unwrap();
524+
525+ let shutdown_session = project.update(cx, |project, cx| {
526+ project.dap_store().update(cx, |dap_store, cx| {
527+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
528+ })
529+ });
530+
531+ shutdown_session.await.unwrap();
532+}
533+
534+#[gpui::test]
535+async fn test_handle_start_debugging_reverse_request(
536+ executor: BackgroundExecutor,
537+ cx: &mut TestAppContext,
538+) {
539+ init_test(cx);
540+
541+ let send_response = Arc::new(AtomicBool::new(false));
542+
543+ let fs = FakeFs::new(executor.clone());
544+
545+ fs.insert_tree(
546+ "/project",
547+ json!({
548+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
549+ }),
550+ )
551+ .await;
552+
553+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
554+ let workspace = init_test_workspace(&project, cx).await;
555+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
556+
557+ let task = project.update(cx, |project, cx| {
558+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
559+ });
560+
561+ let session = task.await.unwrap();
562+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
563+
564+ client
565+ .on_request::<dap::requests::Threads, _>(move |_, _| {
566+ Ok(dap::ThreadsResponse {
567+ threads: vec![dap::Thread {
568+ id: 1,
569+ name: "Thread 1".into(),
570+ }],
571+ })
572+ })
573+ .await;
574+
575+ client
576+ .on_response::<StartDebugging, _>({
577+ let send_response = send_response.clone();
578+ move |response| {
579+ send_response.store(true, Ordering::SeqCst);
580+
581+ assert!(response.success);
582+ assert!(response.body.is_some());
583+ }
584+ })
585+ .await;
586+
587+ client
588+ .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
589+ configuration: json!({}),
590+ request: StartDebuggingRequestArgumentsRequest::Launch,
591+ })
592+ .await;
593+
594+ cx.run_until_parked();
595+
596+ let child_session = project.update(cx, |project, cx| {
597+ project
598+ .dap_store()
599+ .read(cx)
600+ .session_by_id(SessionId(1))
601+ .unwrap()
602+ });
603+ let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
604+
605+ client
606+ .on_request::<dap::requests::Threads, _>(move |_, _| {
607+ Ok(dap::ThreadsResponse {
608+ threads: vec![dap::Thread {
609+ id: 1,
610+ name: "Thread 1".into(),
611+ }],
612+ })
613+ })
614+ .await;
615+
616+ child_client
617+ .on_request::<Disconnect, _>(move |_, _| Ok(()))
618+ .await;
619+
620+ child_client
621+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
622+ reason: dap::StoppedEventReason::Pause,
623+ description: None,
624+ thread_id: Some(2),
625+ preserve_focus_hint: None,
626+ text: None,
627+ all_threads_stopped: None,
628+ hit_breakpoint_ids: None,
629+ }))
630+ .await;
631+
632+ cx.run_until_parked();
633+
634+ assert!(
635+ send_response.load(std::sync::atomic::Ordering::SeqCst),
636+ "Expected to receive response from reverse request"
637+ );
638+
639+ let shutdown_session = project.update(cx, |project, cx| {
640+ project.dap_store().update(cx, |dap_store, cx| {
641+ dap_store.shutdown_session(child_session.read(cx).session_id(), cx)
642+ })
643+ });
644+
645+ shutdown_session.await.unwrap();
646+}
647+
648+#[gpui::test]
649+async fn test_debug_panel_item_thread_status_reset_on_failure(
650+ executor: BackgroundExecutor,
651+ cx: &mut TestAppContext,
652+) {
653+ init_test(cx);
654+
655+ let fs = FakeFs::new(executor.clone());
656+
657+ fs.insert_tree(
658+ "/project",
659+ json!({
660+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
661+ }),
662+ )
663+ .await;
664+
665+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
666+ let workspace = init_test_workspace(&project, cx).await;
667+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
668+
669+ let task = project.update(cx, |project, cx| {
670+ project.start_debug_session(
671+ dap::test_config(
672+ DebugRequestType::Launch,
673+ None,
674+ Some(dap::Capabilities {
675+ supports_step_back: Some(true),
676+ ..Default::default()
677+ }),
678+ ),
679+ cx,
680+ )
681+ });
682+
683+ let session = task.await.unwrap();
684+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
685+ const THREAD_ID_NUM: u64 = 1;
686+
687+ client
688+ .on_request::<dap::requests::Threads, _>(move |_, _| {
689+ Ok(dap::ThreadsResponse {
690+ threads: vec![dap::Thread {
691+ id: THREAD_ID_NUM,
692+ name: "Thread 1".into(),
693+ }],
694+ })
695+ })
696+ .await;
697+
698+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
699+
700+ client
701+ .on_request::<StackTrace, _>(move |_, _| {
702+ Ok(dap::StackTraceResponse {
703+ stack_frames: Vec::default(),
704+ total_frames: None,
705+ })
706+ })
707+ .await;
708+
709+ client
710+ .on_request::<Next, _>(move |_, _| {
711+ Err(ErrorResponse {
712+ error: Some(dap::Message {
713+ id: 1,
714+ format: "error".into(),
715+ variables: None,
716+ send_telemetry: None,
717+ show_user: None,
718+ url: None,
719+ url_label: None,
720+ }),
721+ })
722+ })
723+ .await;
724+
725+ client
726+ .on_request::<StepOut, _>(move |_, _| {
727+ Err(ErrorResponse {
728+ error: Some(dap::Message {
729+ id: 1,
730+ format: "error".into(),
731+ variables: None,
732+ send_telemetry: None,
733+ show_user: None,
734+ url: None,
735+ url_label: None,
736+ }),
737+ })
738+ })
739+ .await;
740+
741+ client
742+ .on_request::<StepIn, _>(move |_, _| {
743+ Err(ErrorResponse {
744+ error: Some(dap::Message {
745+ id: 1,
746+ format: "error".into(),
747+ variables: None,
748+ send_telemetry: None,
749+ show_user: None,
750+ url: None,
751+ url_label: None,
752+ }),
753+ })
754+ })
755+ .await;
756+
757+ client
758+ .on_request::<StepBack, _>(move |_, _| {
759+ Err(ErrorResponse {
760+ error: Some(dap::Message {
761+ id: 1,
762+ format: "error".into(),
763+ variables: None,
764+ send_telemetry: None,
765+ show_user: None,
766+ url: None,
767+ url_label: None,
768+ }),
769+ })
770+ })
771+ .await;
772+
773+ client
774+ .on_request::<Continue, _>(move |_, _| {
775+ Err(ErrorResponse {
776+ error: Some(dap::Message {
777+ id: 1,
778+ format: "error".into(),
779+ variables: None,
780+ send_telemetry: None,
781+ show_user: None,
782+ url: None,
783+ url_label: None,
784+ }),
785+ })
786+ })
787+ .await;
788+
789+ client
790+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
791+ reason: dap::StoppedEventReason::Pause,
792+ description: None,
793+ thread_id: Some(1),
794+ preserve_focus_hint: None,
795+ text: None,
796+ all_threads_stopped: None,
797+ hit_breakpoint_ids: None,
798+ }))
799+ .await;
800+
801+ let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
802+ item.mode()
803+ .as_running()
804+ .expect("Session should be running by this point")
805+ .clone()
806+ });
807+
808+ cx.run_until_parked();
809+ let thread_id = ThreadId(1);
810+
811+ for operation in &[
812+ "step_over",
813+ "continue_thread",
814+ "step_back",
815+ "step_in",
816+ "step_out",
817+ ] {
818+ running_state.update(cx, |running_state, cx| match *operation {
819+ "step_over" => running_state.step_over(cx),
820+ "continue_thread" => running_state.continue_thread(cx),
821+ "step_back" => running_state.step_back(cx),
822+ "step_in" => running_state.step_in(cx),
823+ "step_out" => running_state.step_out(cx),
824+ _ => unreachable!(),
825+ });
826+
827+ // Check that we step the thread status to the correct intermediate state
828+ running_state.update(cx, |running_state, cx| {
829+ assert_eq!(
830+ running_state
831+ .thread_status(cx)
832+ .expect("There should be an active thread selected"),
833+ match *operation {
834+ "continue_thread" => ThreadStatus::Running,
835+ _ => ThreadStatus::Stepping,
836+ },
837+ "Thread status was not set to correct intermediate state after {} request",
838+ operation
839+ );
840+ });
841+
842+ cx.run_until_parked();
843+
844+ running_state.update(cx, |running_state, cx| {
845+ assert_eq!(
846+ running_state
847+ .thread_status(cx)
848+ .expect("There should be an active thread selected"),
849+ ThreadStatus::Stopped,
850+ "Thread status not reset to Stopped after failed {}",
851+ operation
852+ );
853+
854+ // update state to running, so we can test it actually changes the status back to stopped
855+ running_state
856+ .session()
857+ .update(cx, |session, cx| session.continue_thread(thread_id, cx));
858+ });
859+ }
860+
861+ let shutdown_session = project.update(cx, |project, cx| {
862+ project.dap_store().update(cx, |dap_store, cx| {
863+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
864+ })
865+ });
866+
867+ shutdown_session.await.unwrap();
868+}
869+
870+#[gpui::test]
871+async fn test_send_breakpoints_when_editor_has_been_saved(
872+ executor: BackgroundExecutor,
873+ cx: &mut TestAppContext,
874+) {
875+ init_test(cx);
876+
877+ let fs = FakeFs::new(executor.clone());
878+
879+ fs.insert_tree(
880+ path!("/project"),
881+ json!({
882+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
883+ }),
884+ )
885+ .await;
886+
887+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
888+ let workspace = init_test_workspace(&project, cx).await;
889+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
890+ let project_path = Path::new(path!("/project"));
891+ let worktree = project
892+ .update(cx, |project, cx| project.find_worktree(project_path, cx))
893+ .expect("This worktree should exist in project")
894+ .0;
895+
896+ let worktree_id = workspace
897+ .update(cx, |_, _, cx| worktree.read(cx).id())
898+ .unwrap();
899+
900+ let task = project.update(cx, |project, cx| {
901+ project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
902+ });
903+
904+ let session = task.await.unwrap();
905+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
906+
907+ let buffer = project
908+ .update(cx, |project, cx| {
909+ project.open_buffer((worktree_id, "main.rs"), cx)
910+ })
911+ .await
912+ .unwrap();
913+
914+ let (editor, cx) = cx.add_window_view(|window, cx| {
915+ Editor::new(
916+ EditorMode::Full,
917+ MultiBuffer::build_from_buffer(buffer, cx),
918+ Some(project.clone()),
919+ window,
920+ cx,
921+ )
922+ });
923+
924+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
925+
926+ client
927+ .on_request::<StackTrace, _>(move |_, _| {
928+ Ok(dap::StackTraceResponse {
929+ stack_frames: Vec::default(),
930+ total_frames: None,
931+ })
932+ })
933+ .await;
934+
935+ client
936+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
937+ reason: dap::StoppedEventReason::Pause,
938+ description: None,
939+ thread_id: Some(1),
940+ preserve_focus_hint: None,
941+ text: None,
942+ all_threads_stopped: None,
943+ hit_breakpoint_ids: None,
944+ }))
945+ .await;
946+
947+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
948+ client
949+ .on_request::<SetBreakpoints, _>({
950+ let called_set_breakpoints = called_set_breakpoints.clone();
951+ move |_, args| {
952+ assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
953+ assert_eq!(
954+ vec![SourceBreakpoint {
955+ line: 2,
956+ column: None,
957+ condition: None,
958+ hit_condition: None,
959+ log_message: None,
960+ mode: None
961+ }],
962+ args.breakpoints.unwrap()
963+ );
964+ assert!(!args.source_modified.unwrap());
965+
966+ called_set_breakpoints.store(true, Ordering::SeqCst);
967+
968+ Ok(dap::SetBreakpointsResponse {
969+ breakpoints: Vec::default(),
970+ })
971+ }
972+ })
973+ .await;
974+
975+ editor.update_in(cx, |editor, window, cx| {
976+ editor.move_down(&actions::MoveDown, window, cx);
977+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
978+ });
979+
980+ cx.run_until_parked();
981+
982+ assert!(
983+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
984+ "SetBreakpoint request must be called"
985+ );
986+
987+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
988+ client
989+ .on_request::<SetBreakpoints, _>({
990+ let called_set_breakpoints = called_set_breakpoints.clone();
991+ move |_, args| {
992+ assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
993+ assert_eq!(
994+ vec![SourceBreakpoint {
995+ line: 3,
996+ column: None,
997+ condition: None,
998+ hit_condition: None,
999+ log_message: None,
1000+ mode: None
1001+ }],
1002+ args.breakpoints.unwrap()
1003+ );
1004+ assert!(args.source_modified.unwrap());
1005+
1006+ called_set_breakpoints.store(true, Ordering::SeqCst);
1007+
1008+ Ok(dap::SetBreakpointsResponse {
1009+ breakpoints: Vec::default(),
1010+ })
1011+ }
1012+ })
1013+ .await;
1014+
1015+ editor.update_in(cx, |editor, window, cx| {
1016+ editor.move_up(&actions::MoveUp, window, cx);
1017+ editor.insert("new text\n", window, cx);
1018+ });
1019+
1020+ editor
1021+ .update_in(cx, |editor, window, cx| {
1022+ editor.save(true, project.clone(), window, cx)
1023+ })
1024+ .await
1025+ .unwrap();
1026+
1027+ cx.run_until_parked();
1028+
1029+ assert!(
1030+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1031+ "SetBreakpoint request must be called after editor is saved"
1032+ );
1033+
1034+ let shutdown_session = project.update(cx, |project, cx| {
1035+ project.dap_store().update(cx, |dap_store, cx| {
1036+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
1037+ })
1038+ });
1039+
1040+ shutdown_session.await.unwrap();
1041+}
1042+
1043+#[gpui::test]
1044+async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1045+ executor: BackgroundExecutor,
1046+ cx: &mut TestAppContext,
1047+) {
1048+ init_test(cx);
1049+
1050+ let fs = FakeFs::new(executor.clone());
1051+
1052+ fs.insert_tree(
1053+ "/project",
1054+ json!({
1055+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
1056+ }),
1057+ )
1058+ .await;
1059+
1060+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
1061+ let workspace = init_test_workspace(&project, cx).await;
1062+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
1063+
1064+ let task = project.update(cx, |project, cx| {
1065+ project.start_debug_session(
1066+ dap::test_config(DebugRequestType::Launch, Some(true), None),
1067+ cx,
1068+ )
1069+ });
1070+
1071+ assert!(
1072+ task.await.is_err(),
1073+ "Session should failed to start if launch request fails"
1074+ );
1075+
1076+ cx.run_until_parked();
1077+
1078+ project.update(cx, |project, cx| {
1079+ assert!(
1080+ project.dap_store().read(cx).sessions().count() == 0,
1081+ "Session wouldn't exist if it was shutdown"
1082+ );
1083+ });
1084+}
crates/debugger_ui/src/tests/module_list.rs+262−0
@@ -0,0 +1,262 @@
1+use crate::{
2+ debugger_panel::DebugPanel,
3+ session::ThreadItem,
4+ tests::{active_debug_session_panel, init_test, init_test_workspace},
5+};
6+use dap::{
7+ requests::{Modules, StackTrace, Threads},
8+ DebugRequestType, StoppedEvent,
9+};
10+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
11+use project::{FakeFs, Project};
12+use std::sync::{
13+ atomic::{AtomicBool, AtomicI32, Ordering},
14+ Arc,
15+};
16+
17+#[gpui::test]
18+async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19+ init_test(cx);
20+
21+ let fs = FakeFs::new(executor.clone());
22+
23+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
24+ let workspace = init_test_workspace(&project, cx).await;
25+ workspace
26+ .update(cx, |workspace, window, cx| {
27+ workspace.focus_panel::<DebugPanel>(window, cx);
28+ })
29+ .unwrap();
30+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
31+
32+ let task = project.update(cx, |project, cx| {
33+ project.start_debug_session(
34+ dap::test_config(
35+ DebugRequestType::Launch,
36+ None,
37+ Some(dap::Capabilities {
38+ supports_modules_request: Some(true),
39+ ..Default::default()
40+ }),
41+ ),
42+ cx,
43+ )
44+ });
45+
46+ let session = task.await.unwrap();
47+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
48+
49+ client
50+ .on_request::<StackTrace, _>(move |_, args| {
51+ assert!(args.thread_id == 1);
52+ Ok(dap::StackTraceResponse {
53+ stack_frames: Vec::default(),
54+ total_frames: None,
55+ })
56+ })
57+ .await;
58+
59+ let called_modules = Arc::new(AtomicBool::new(false));
60+ let modules = vec![
61+ dap::Module {
62+ id: dap::ModuleId::Number(1),
63+ name: "First Module".into(),
64+ address_range: None,
65+ date_time_stamp: None,
66+ path: None,
67+ symbol_file_path: None,
68+ symbol_status: None,
69+ version: None,
70+ is_optimized: None,
71+ is_user_code: None,
72+ },
73+ dap::Module {
74+ id: dap::ModuleId::Number(2),
75+ name: "Second Module".into(),
76+ address_range: None,
77+ date_time_stamp: None,
78+ path: None,
79+ symbol_file_path: None,
80+ symbol_status: None,
81+ version: None,
82+ is_optimized: None,
83+ is_user_code: None,
84+ },
85+ ];
86+
87+ client
88+ .on_request::<Threads, _>(move |_, _| {
89+ Ok(dap::ThreadsResponse {
90+ threads: vec![dap::Thread {
91+ id: 1,
92+ name: "Thread 1".into(),
93+ }],
94+ })
95+ })
96+ .await;
97+
98+ client
99+ .on_request::<Modules, _>({
100+ let called_modules = called_modules.clone();
101+ let modules_request_count = AtomicI32::new(0);
102+ let modules = modules.clone();
103+ move |_, _| {
104+ modules_request_count.fetch_add(1, Ordering::SeqCst);
105+ assert_eq!(
106+ 1,
107+ modules_request_count.load(Ordering::SeqCst),
108+ "This request should only be called once from the host"
109+ );
110+ called_modules.store(true, Ordering::SeqCst);
111+
112+ Ok(dap::ModulesResponse {
113+ modules: modules.clone(),
114+ total_modules: Some(2u64),
115+ })
116+ }
117+ })
118+ .await;
119+
120+ client
121+ .fake_event(dap::messages::Events::Stopped(StoppedEvent {
122+ reason: dap::StoppedEventReason::Pause,
123+ description: None,
124+ thread_id: Some(1),
125+ preserve_focus_hint: None,
126+ text: None,
127+ all_threads_stopped: None,
128+ hit_breakpoint_ids: None,
129+ }))
130+ .await;
131+
132+ cx.run_until_parked();
133+
134+ let running_state =
135+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
136+ cx.focus_self(window);
137+ item.mode()
138+ .as_running()
139+ .expect("Session should be running by this point")
140+ .clone()
141+ });
142+
143+ assert!(
144+ !called_modules.load(std::sync::atomic::Ordering::SeqCst),
145+ "Request Modules shouldn't be called before it's needed"
146+ );
147+
148+ running_state.update(cx, |state, cx| {
149+ state.set_thread_item(ThreadItem::Modules, cx);
150+ cx.refresh_windows();
151+ });
152+
153+ cx.run_until_parked();
154+
155+ assert!(
156+ called_modules.load(std::sync::atomic::Ordering::SeqCst),
157+ "Request Modules should be called because a user clicked on the module list"
158+ );
159+
160+ active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
161+ running_state.update(cx, |state, cx| {
162+ state.set_thread_item(ThreadItem::Modules, cx)
163+ });
164+ let actual_modules = running_state.update(cx, |state, cx| {
165+ state.module_list().update(cx, |list, cx| list.modules(cx))
166+ });
167+
168+ assert_eq!(modules, actual_modules);
169+ });
170+
171+ // Test all module events now
172+ // New Module
173+ // Changed
174+ // Removed
175+
176+ let new_module = dap::Module {
177+ id: dap::ModuleId::Number(3),
178+ name: "Third Module".into(),
179+ address_range: None,
180+ date_time_stamp: None,
181+ path: None,
182+ symbol_file_path: None,
183+ symbol_status: None,
184+ version: None,
185+ is_optimized: None,
186+ is_user_code: None,
187+ };
188+
189+ client
190+ .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
191+ reason: dap::ModuleEventReason::New,
192+ module: new_module.clone(),
193+ }))
194+ .await;
195+
196+ cx.run_until_parked();
197+
198+ active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
199+ let actual_modules = running_state.update(cx, |state, cx| {
200+ state.module_list().update(cx, |list, cx| list.modules(cx))
201+ });
202+ assert_eq!(actual_modules.len(), 3);
203+ assert!(actual_modules.contains(&new_module));
204+ });
205+
206+ let changed_module = dap::Module {
207+ id: dap::ModuleId::Number(2),
208+ name: "Modified Second Module".into(),
209+ address_range: None,
210+ date_time_stamp: None,
211+ path: None,
212+ symbol_file_path: None,
213+ symbol_status: None,
214+ version: None,
215+ is_optimized: None,
216+ is_user_code: None,
217+ };
218+
219+ client
220+ .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
221+ reason: dap::ModuleEventReason::Changed,
222+ module: changed_module.clone(),
223+ }))
224+ .await;
225+
226+ cx.run_until_parked();
227+
228+ active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
229+ let actual_modules = running_state.update(cx, |state, cx| {
230+ state.module_list().update(cx, |list, cx| list.modules(cx))
231+ });
232+
233+ assert_eq!(actual_modules.len(), 3);
234+ assert!(actual_modules.contains(&changed_module));
235+ });
236+
237+ client
238+ .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
239+ reason: dap::ModuleEventReason::Removed,
240+ module: changed_module.clone(),
241+ }))
242+ .await;
243+
244+ cx.run_until_parked();
245+
246+ active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
247+ let actual_modules = running_state.update(cx, |state, cx| {
248+ state.module_list().update(cx, |list, cx| list.modules(cx))
249+ });
250+
251+ assert_eq!(actual_modules.len(), 2);
252+ assert!(!actual_modules.contains(&changed_module));
253+ });
254+
255+ let shutdown_session = project.update(cx, |project, cx| {
256+ project.dap_store().update(cx, |dap_store, cx| {
257+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
258+ })
259+ });
260+
261+ shutdown_session.await.unwrap();
262+}
crates/debugger_ui/src/tests/stack_frame_list.rs+845−0
@@ -0,0 +1,845 @@
1+use crate::{
2+ debugger_panel::DebugPanel,
3+ session::running::stack_frame_list::StackFrameEntry,
4+ tests::{active_debug_session_panel, init_test, init_test_workspace},
5+};
6+use dap::{
7+ requests::{StackTrace, Threads},
8+ StackFrame,
9+};
10+use editor::{Editor, ToPoint as _};
11+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
12+use project::{FakeFs, Project};
13+use serde_json::json;
14+use std::sync::Arc;
15+use unindent::Unindent as _;
16+use util::path;
17+
18+#[gpui::test]
19+async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
20+ executor: BackgroundExecutor,
21+ cx: &mut TestAppContext,
22+) {
23+ init_test(cx);
24+
25+ let fs = FakeFs::new(executor.clone());
26+
27+ let test_file_content = r#"
28+ import { SOME_VALUE } './module.js';
29+
30+ console.log(SOME_VALUE);
31+ "#
32+ .unindent();
33+
34+ let module_file_content = r#"
35+ export SOME_VALUE = 'some value';
36+ "#
37+ .unindent();
38+
39+ fs.insert_tree(
40+ path!("/project"),
41+ json!({
42+ "src": {
43+ "test.js": test_file_content,
44+ "module.js": module_file_content,
45+ }
46+ }),
47+ )
48+ .await;
49+
50+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
51+ let workspace = init_test_workspace(&project, cx).await;
52+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
53+
54+ let task = project.update(cx, |project, cx| {
55+ project.start_debug_session(
56+ dap::test_config(dap::DebugRequestType::Launch, None, None),
57+ cx,
58+ )
59+ });
60+
61+ let session = task.await.unwrap();
62+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
63+
64+ client
65+ .on_request::<Threads, _>(move |_, _| {
66+ Ok(dap::ThreadsResponse {
67+ threads: vec![dap::Thread {
68+ id: 1,
69+ name: "Thread 1".into(),
70+ }],
71+ })
72+ })
73+ .await;
74+
75+ let stack_frames = vec![
76+ StackFrame {
77+ id: 1,
78+ name: "Stack Frame 1".into(),
79+ source: Some(dap::Source {
80+ name: Some("test.js".into()),
81+ path: Some(path!("/project/src/test.js").into()),
82+ source_reference: None,
83+ presentation_hint: None,
84+ origin: None,
85+ sources: None,
86+ adapter_data: None,
87+ checksums: None,
88+ }),
89+ line: 3,
90+ column: 1,
91+ end_line: None,
92+ end_column: None,
93+ can_restart: None,
94+ instruction_pointer_reference: None,
95+ module_id: None,
96+ presentation_hint: None,
97+ },
98+ StackFrame {
99+ id: 2,
100+ name: "Stack Frame 2".into(),
101+ source: Some(dap::Source {
102+ name: Some("module.js".into()),
103+ path: Some(path!("/project/src/module.js").into()),
104+ source_reference: None,
105+ presentation_hint: None,
106+ origin: None,
107+ sources: None,
108+ adapter_data: None,
109+ checksums: None,
110+ }),
111+ line: 1,
112+ column: 1,
113+ end_line: None,
114+ end_column: None,
115+ can_restart: None,
116+ instruction_pointer_reference: None,
117+ module_id: None,
118+ presentation_hint: None,
119+ },
120+ ];
121+
122+ client
123+ .on_request::<StackTrace, _>({
124+ let stack_frames = Arc::new(stack_frames.clone());
125+ move |_, args| {
126+ assert_eq!(1, args.thread_id);
127+
128+ Ok(dap::StackTraceResponse {
129+ stack_frames: (*stack_frames).clone(),
130+ total_frames: None,
131+ })
132+ }
133+ })
134+ .await;
135+
136+ client
137+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
138+ reason: dap::StoppedEventReason::Pause,
139+ description: None,
140+ thread_id: Some(1),
141+ preserve_focus_hint: None,
142+ text: None,
143+ all_threads_stopped: None,
144+ hit_breakpoint_ids: None,
145+ }))
146+ .await;
147+
148+ cx.run_until_parked();
149+
150+ // trigger to load threads
151+ active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
152+ session
153+ .mode()
154+ .as_running()
155+ .unwrap()
156+ .update(cx, |running_state, cx| {
157+ running_state
158+ .session()
159+ .update(cx, |session, cx| session.threads(cx));
160+ });
161+ });
162+
163+ cx.run_until_parked();
164+
165+ // select first thread
166+ active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
167+ session
168+ .mode()
169+ .as_running()
170+ .unwrap()
171+ .update(cx, |running_state, cx| {
172+ running_state.select_current_thread(
173+ &running_state
174+ .session()
175+ .update(cx, |session, cx| session.threads(cx)),
176+ cx,
177+ );
178+ });
179+ });
180+
181+ cx.run_until_parked();
182+
183+ active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
184+ let stack_frame_list = session
185+ .mode()
186+ .as_running()
187+ .unwrap()
188+ .update(cx, |state, _| state.stack_frame_list().clone());
189+
190+ stack_frame_list.update(cx, |stack_frame_list, cx| {
191+ assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
192+ assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
193+ });
194+ });
195+
196+ let shutdown_session = project.update(cx, |project, cx| {
197+ project.dap_store().update(cx, |dap_store, cx| {
198+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
199+ })
200+ });
201+
202+ shutdown_session.await.unwrap();
203+}
204+
205+#[gpui::test]
206+async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
207+ init_test(cx);
208+
209+ let fs = FakeFs::new(executor.clone());
210+
211+ let test_file_content = r#"
212+ import { SOME_VALUE } './module.js';
213+
214+ console.log(SOME_VALUE);
215+ "#
216+ .unindent();
217+
218+ let module_file_content = r#"
219+ export SOME_VALUE = 'some value';
220+ "#
221+ .unindent();
222+
223+ fs.insert_tree(
224+ path!("/project"),
225+ json!({
226+ "src": {
227+ "test.js": test_file_content,
228+ "module.js": module_file_content,
229+ }
230+ }),
231+ )
232+ .await;
233+
234+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
235+ let workspace = init_test_workspace(&project, cx).await;
236+ let _ = workspace.update(cx, |workspace, window, cx| {
237+ workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
238+ });
239+
240+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
241+
242+ let task = project.update(cx, |project, cx| {
243+ project.start_debug_session(
244+ dap::test_config(dap::DebugRequestType::Launch, None, None),
245+ cx,
246+ )
247+ });
248+
249+ let session = task.await.unwrap();
250+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
251+
252+ client
253+ .on_request::<Threads, _>(move |_, _| {
254+ Ok(dap::ThreadsResponse {
255+ threads: vec![dap::Thread {
256+ id: 1,
257+ name: "Thread 1".into(),
258+ }],
259+ })
260+ })
261+ .await;
262+
263+ let stack_frames = vec![
264+ StackFrame {
265+ id: 1,
266+ name: "Stack Frame 1".into(),
267+ source: Some(dap::Source {
268+ name: Some("test.js".into()),
269+ path: Some(path!("/project/src/test.js").into()),
270+ source_reference: None,
271+ presentation_hint: None,
272+ origin: None,
273+ sources: None,
274+ adapter_data: None,
275+ checksums: None,
276+ }),
277+ line: 3,
278+ column: 1,
279+ end_line: None,
280+ end_column: None,
281+ can_restart: None,
282+ instruction_pointer_reference: None,
283+ module_id: None,
284+ presentation_hint: None,
285+ },
286+ StackFrame {
287+ id: 2,
288+ name: "Stack Frame 2".into(),
289+ source: Some(dap::Source {
290+ name: Some("module.js".into()),
291+ path: Some(path!("/project/src/module.js").into()),
292+ source_reference: None,
293+ presentation_hint: None,
294+ origin: None,
295+ sources: None,
296+ adapter_data: None,
297+ checksums: None,
298+ }),
299+ line: 1,
300+ column: 1,
301+ end_line: None,
302+ end_column: None,
303+ can_restart: None,
304+ instruction_pointer_reference: None,
305+ module_id: None,
306+ presentation_hint: None,
307+ },
308+ ];
309+
310+ client
311+ .on_request::<StackTrace, _>({
312+ let stack_frames = Arc::new(stack_frames.clone());
313+ move |_, args| {
314+ assert_eq!(1, args.thread_id);
315+
316+ Ok(dap::StackTraceResponse {
317+ stack_frames: (*stack_frames).clone(),
318+ total_frames: None,
319+ })
320+ }
321+ })
322+ .await;
323+
324+ client
325+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
326+ reason: dap::StoppedEventReason::Pause,
327+ description: None,
328+ thread_id: Some(1),
329+ preserve_focus_hint: None,
330+ text: None,
331+ all_threads_stopped: None,
332+ hit_breakpoint_ids: None,
333+ }))
334+ .await;
335+
336+ cx.run_until_parked();
337+
338+ // trigger threads to load
339+ active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
340+ session
341+ .mode()
342+ .as_running()
343+ .unwrap()
344+ .update(cx, |running_state, cx| {
345+ running_state
346+ .session()
347+ .update(cx, |session, cx| session.threads(cx));
348+ });
349+ });
350+
351+ cx.run_until_parked();
352+
353+ // select first thread
354+ active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
355+ session
356+ .mode()
357+ .as_running()
358+ .unwrap()
359+ .update(cx, |running_state, cx| {
360+ running_state.select_current_thread(
361+ &running_state
362+ .session()
363+ .update(cx, |session, cx| session.threads(cx)),
364+ cx,
365+ );
366+ });
367+ });
368+
369+ cx.run_until_parked();
370+
371+ workspace
372+ .update(cx, |workspace, window, cx| {
373+ let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
374+ assert_eq!(1, editors.len());
375+
376+ let project_path = editors[0]
377+ .update(cx, |editor, cx| editor.project_path(cx))
378+ .unwrap();
379+ let expected = if cfg!(target_os = "windows") {
380+ "src\\test.js"
381+ } else {
382+ "src/test.js"
383+ };
384+ assert_eq!(expected, project_path.path.to_string_lossy());
385+ assert_eq!(test_file_content, editors[0].read(cx).text(cx));
386+ assert_eq!(
387+ vec![2..3],
388+ editors[0].update(cx, |editor, cx| {
389+ let snapshot = editor.snapshot(window, cx);
390+
391+ editor
392+ .highlighted_rows::<editor::DebugCurrentRowHighlight>()
393+ .map(|(range, _)| {
394+ let start = range.start.to_point(&snapshot.buffer_snapshot);
395+ let end = range.end.to_point(&snapshot.buffer_snapshot);
396+ start.row..end.row
397+ })
398+ .collect::<Vec<_>>()
399+ })
400+ );
401+ })
402+ .unwrap();
403+
404+ let stack_frame_list = workspace
405+ .update(cx, |workspace, _window, cx| {
406+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
407+ let active_debug_panel_item = debug_panel
408+ .update(cx, |this, cx| this.active_session(cx))
409+ .unwrap();
410+
411+ active_debug_panel_item
412+ .read(cx)
413+ .mode()
414+ .as_running()
415+ .unwrap()
416+ .read(cx)
417+ .stack_frame_list()
418+ .clone()
419+ })
420+ .unwrap();
421+
422+ stack_frame_list.update(cx, |stack_frame_list, cx| {
423+ assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
424+ assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
425+ });
426+
427+ // select second stack frame
428+ stack_frame_list
429+ .update_in(cx, |stack_frame_list, window, cx| {
430+ stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
431+ })
432+ .await
433+ .unwrap();
434+
435+ cx.run_until_parked();
436+
437+ stack_frame_list.update(cx, |stack_frame_list, cx| {
438+ assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
439+ assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
440+ });
441+
442+ let _ = workspace.update(cx, |workspace, window, cx| {
443+ let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
444+ assert_eq!(1, editors.len());
445+
446+ let project_path = editors[0]
447+ .update(cx, |editor, cx| editor.project_path(cx))
448+ .unwrap();
449+ let expected = if cfg!(target_os = "windows") {
450+ "src\\module.js"
451+ } else {
452+ "src/module.js"
453+ };
454+ assert_eq!(expected, project_path.path.to_string_lossy());
455+ assert_eq!(module_file_content, editors[0].read(cx).text(cx));
456+ assert_eq!(
457+ vec![0..1],
458+ editors[0].update(cx, |editor, cx| {
459+ let snapshot = editor.snapshot(window, cx);
460+
461+ editor
462+ .highlighted_rows::<editor::DebugCurrentRowHighlight>()
463+ .map(|(range, _)| {
464+ let start = range.start.to_point(&snapshot.buffer_snapshot);
465+ let end = range.end.to_point(&snapshot.buffer_snapshot);
466+ start.row..end.row
467+ })
468+ .collect::<Vec<_>>()
469+ })
470+ );
471+ });
472+
473+ let shutdown_session = project.update(cx, |project, cx| {
474+ project.dap_store().update(cx, |dap_store, cx| {
475+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
476+ })
477+ });
478+
479+ shutdown_session.await.unwrap();
480+}
481+
482+#[gpui::test]
483+async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
484+ init_test(cx);
485+
486+ let fs = FakeFs::new(executor.clone());
487+
488+ let test_file_content = r#"
489+ import { SOME_VALUE } './module.js';
490+
491+ console.log(SOME_VALUE);
492+ "#
493+ .unindent();
494+
495+ let module_file_content = r#"
496+ export SOME_VALUE = 'some value';
497+ "#
498+ .unindent();
499+
500+ fs.insert_tree(
501+ path!("/project"),
502+ json!({
503+ "src": {
504+ "test.js": test_file_content,
505+ "module.js": module_file_content,
506+ }
507+ }),
508+ )
509+ .await;
510+
511+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
512+ let workspace = init_test_workspace(&project, cx).await;
513+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
514+
515+ let task = project.update(cx, |project, cx| {
516+ project.start_debug_session(
517+ dap::test_config(dap::DebugRequestType::Launch, None, None),
518+ cx,
519+ )
520+ });
521+
522+ let session = task.await.unwrap();
523+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
524+
525+ client
526+ .on_request::<Threads, _>(move |_, _| {
527+ Ok(dap::ThreadsResponse {
528+ threads: vec![dap::Thread {
529+ id: 1,
530+ name: "Thread 1".into(),
531+ }],
532+ })
533+ })
534+ .await;
535+
536+ let stack_frames = vec![
537+ StackFrame {
538+ id: 1,
539+ name: "Stack Frame 1".into(),
540+ source: Some(dap::Source {
541+ name: Some("test.js".into()),
542+ path: Some(path!("/project/src/test.js").into()),
543+ source_reference: None,
544+ presentation_hint: None,
545+ origin: None,
546+ sources: None,
547+ adapter_data: None,
548+ checksums: None,
549+ }),
550+ line: 3,
551+ column: 1,
552+ end_line: None,
553+ end_column: None,
554+ can_restart: None,
555+ instruction_pointer_reference: None,
556+ module_id: None,
557+ presentation_hint: None,
558+ },
559+ StackFrame {
560+ id: 2,
561+ name: "Stack Frame 2".into(),
562+ source: Some(dap::Source {
563+ name: Some("module.js".into()),
564+ path: Some(path!("/project/src/module.js").into()),
565+ source_reference: None,
566+ presentation_hint: None,
567+ origin: Some("ignored".into()),
568+ sources: None,
569+ adapter_data: None,
570+ checksums: None,
571+ }),
572+ line: 1,
573+ column: 1,
574+ end_line: None,
575+ end_column: None,
576+ can_restart: None,
577+ instruction_pointer_reference: None,
578+ module_id: None,
579+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
580+ },
581+ StackFrame {
582+ id: 3,
583+ name: "Stack Frame 3".into(),
584+ source: Some(dap::Source {
585+ name: Some("module.js".into()),
586+ path: Some(path!("/project/src/module.js").into()),
587+ source_reference: None,
588+ presentation_hint: None,
589+ origin: Some("ignored".into()),
590+ sources: None,
591+ adapter_data: None,
592+ checksums: None,
593+ }),
594+ line: 1,
595+ column: 1,
596+ end_line: None,
597+ end_column: None,
598+ can_restart: None,
599+ instruction_pointer_reference: None,
600+ module_id: None,
601+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
602+ },
603+ StackFrame {
604+ id: 4,
605+ name: "Stack Frame 4".into(),
606+ source: Some(dap::Source {
607+ name: Some("module.js".into()),
608+ path: Some(path!("/project/src/module.js").into()),
609+ source_reference: None,
610+ presentation_hint: None,
611+ origin: None,
612+ sources: None,
613+ adapter_data: None,
614+ checksums: None,
615+ }),
616+ line: 1,
617+ column: 1,
618+ end_line: None,
619+ end_column: None,
620+ can_restart: None,
621+ instruction_pointer_reference: None,
622+ module_id: None,
623+ presentation_hint: None,
624+ },
625+ StackFrame {
626+ id: 5,
627+ name: "Stack Frame 5".into(),
628+ source: Some(dap::Source {
629+ name: Some("module.js".into()),
630+ path: Some(path!("/project/src/module.js").into()),
631+ source_reference: None,
632+ presentation_hint: None,
633+ origin: None,
634+ sources: None,
635+ adapter_data: None,
636+ checksums: None,
637+ }),
638+ line: 1,
639+ column: 1,
640+ end_line: None,
641+ end_column: None,
642+ can_restart: None,
643+ instruction_pointer_reference: None,
644+ module_id: None,
645+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
646+ },
647+ StackFrame {
648+ id: 6,
649+ name: "Stack Frame 6".into(),
650+ source: Some(dap::Source {
651+ name: Some("module.js".into()),
652+ path: Some(path!("/project/src/module.js").into()),
653+ source_reference: None,
654+ presentation_hint: None,
655+ origin: None,
656+ sources: None,
657+ adapter_data: None,
658+ checksums: None,
659+ }),
660+ line: 1,
661+ column: 1,
662+ end_line: None,
663+ end_column: None,
664+ can_restart: None,
665+ instruction_pointer_reference: None,
666+ module_id: None,
667+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
668+ },
669+ StackFrame {
670+ id: 7,
671+ name: "Stack Frame 7".into(),
672+ source: Some(dap::Source {
673+ name: Some("module.js".into()),
674+ path: Some(path!("/project/src/module.js").into()),
675+ source_reference: None,
676+ presentation_hint: None,
677+ origin: None,
678+ sources: None,
679+ adapter_data: None,
680+ checksums: None,
681+ }),
682+ line: 1,
683+ column: 1,
684+ end_line: None,
685+ end_column: None,
686+ can_restart: None,
687+ instruction_pointer_reference: None,
688+ module_id: None,
689+ presentation_hint: None,
690+ },
691+ ];
692+
693+ client
694+ .on_request::<StackTrace, _>({
695+ let stack_frames = Arc::new(stack_frames.clone());
696+ move |_, args| {
697+ assert_eq!(1, args.thread_id);
698+
699+ Ok(dap::StackTraceResponse {
700+ stack_frames: (*stack_frames).clone(),
701+ total_frames: None,
702+ })
703+ }
704+ })
705+ .await;
706+
707+ client
708+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
709+ reason: dap::StoppedEventReason::Pause,
710+ description: None,
711+ thread_id: Some(1),
712+ preserve_focus_hint: None,
713+ text: None,
714+ all_threads_stopped: None,
715+ hit_breakpoint_ids: None,
716+ }))
717+ .await;
718+
719+ cx.run_until_parked();
720+
721+ // trigger threads to load
722+ active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
723+ session
724+ .mode()
725+ .as_running()
726+ .unwrap()
727+ .update(cx, |running_state, cx| {
728+ running_state
729+ .session()
730+ .update(cx, |session, cx| session.threads(cx));
731+ });
732+ });
733+
734+ cx.run_until_parked();
735+
736+ // select first thread
737+ active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
738+ session
739+ .mode()
740+ .as_running()
741+ .unwrap()
742+ .update(cx, |running_state, cx| {
743+ running_state.select_current_thread(
744+ &running_state
745+ .session()
746+ .update(cx, |session, cx| session.threads(cx)),
747+ cx,
748+ );
749+ });
750+ });
751+
752+ cx.run_until_parked();
753+
754+ // trigger stack frames to loaded
755+ active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
756+ let stack_frame_list = debug_panel_item
757+ .mode()
758+ .as_running()
759+ .unwrap()
760+ .update(cx, |state, _| state.stack_frame_list().clone());
761+
762+ stack_frame_list.update(cx, |stack_frame_list, cx| {
763+ stack_frame_list.dap_stack_frames(cx);
764+ });
765+ });
766+
767+ cx.run_until_parked();
768+
769+ active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
770+ let stack_frame_list = debug_panel_item
771+ .mode()
772+ .as_running()
773+ .unwrap()
774+ .update(cx, |state, _| state.stack_frame_list().clone());
775+
776+ stack_frame_list.update(cx, |stack_frame_list, cx| {
777+ stack_frame_list.build_entries(true, window, cx);
778+
779+ assert_eq!(
780+ &vec![
781+ StackFrameEntry::Normal(stack_frames[0].clone()),
782+ StackFrameEntry::Collapsed(vec![
783+ stack_frames[1].clone(),
784+ stack_frames[2].clone()
785+ ]),
786+ StackFrameEntry::Normal(stack_frames[3].clone()),
787+ StackFrameEntry::Collapsed(vec![
788+ stack_frames[4].clone(),
789+ stack_frames[5].clone()
790+ ]),
791+ StackFrameEntry::Normal(stack_frames[6].clone()),
792+ ],
793+ stack_frame_list.entries()
794+ );
795+
796+ stack_frame_list.expand_collapsed_entry(
797+ 1,
798+ &vec![stack_frames[1].clone(), stack_frames[2].clone()],
799+ cx,
800+ );
801+
802+ assert_eq!(
803+ &vec![
804+ StackFrameEntry::Normal(stack_frames[0].clone()),
805+ StackFrameEntry::Normal(stack_frames[1].clone()),
806+ StackFrameEntry::Normal(stack_frames[2].clone()),
807+ StackFrameEntry::Normal(stack_frames[3].clone()),
808+ StackFrameEntry::Collapsed(vec![
809+ stack_frames[4].clone(),
810+ stack_frames[5].clone()
811+ ]),
812+ StackFrameEntry::Normal(stack_frames[6].clone()),
813+ ],
814+ stack_frame_list.entries()
815+ );
816+
817+ stack_frame_list.expand_collapsed_entry(
818+ 4,
819+ &vec![stack_frames[4].clone(), stack_frames[5].clone()],
820+ cx,
821+ );
822+
823+ assert_eq!(
824+ &vec![
825+ StackFrameEntry::Normal(stack_frames[0].clone()),
826+ StackFrameEntry::Normal(stack_frames[1].clone()),
827+ StackFrameEntry::Normal(stack_frames[2].clone()),
828+ StackFrameEntry::Normal(stack_frames[3].clone()),
829+ StackFrameEntry::Normal(stack_frames[4].clone()),
830+ StackFrameEntry::Normal(stack_frames[5].clone()),
831+ StackFrameEntry::Normal(stack_frames[6].clone()),
832+ ],
833+ stack_frame_list.entries()
834+ );
835+ });
836+ });
837+
838+ let shutdown_session = project.update(cx, |project, cx| {
839+ project.dap_store().update(cx, |dap_store, cx| {
840+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
841+ })
842+ });
843+
844+ shutdown_session.await.unwrap();
845+}
crates/debugger_ui/src/tests/variable_list.rs+1759−0
@@ -0,0 +1,1759 @@
1+use std::sync::{
2+ atomic::{AtomicBool, Ordering},
3+ Arc,
4+};
5+
6+use crate::{
7+ session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
8+ tests::{active_debug_session_panel, init_test, init_test_workspace},
9+ DebugPanel,
10+};
11+use collections::HashMap;
12+use dap::{
13+ requests::{Initialize, Launch, Scopes, StackTrace, Variables},
14+ Scope, StackFrame, Variable,
15+};
16+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
17+use menu::{SelectFirst, SelectNext, SelectPrevious};
18+use project::{FakeFs, Project};
19+use serde_json::json;
20+use unindent::Unindent as _;
21+use util::path;
22+
23+/// This only tests fetching one scope and 2 variables for a single stackframe
24+#[gpui::test]
25+async fn test_basic_fetch_initial_scope_and_variables(
26+ executor: BackgroundExecutor,
27+ cx: &mut TestAppContext,
28+) {
29+ init_test(cx);
30+
31+ let fs = FakeFs::new(executor.clone());
32+
33+ let test_file_content = r#"
34+ const variable1 = "Value 1";
35+ const variable2 = "Value 2";
36+ "#
37+ .unindent();
38+
39+ fs.insert_tree(
40+ path!("/project"),
41+ json!({
42+ "src": {
43+ "test.js": test_file_content,
44+ }
45+ }),
46+ )
47+ .await;
48+
49+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
50+ let workspace = init_test_workspace(&project, cx).await;
51+ workspace
52+ .update(cx, |workspace, window, cx| {
53+ workspace.focus_panel::<DebugPanel>(window, cx);
54+ })
55+ .unwrap();
56+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
57+
58+ let task = project.update(cx, |project, cx| {
59+ project.start_debug_session(
60+ dap::test_config(dap::DebugRequestType::Launch, None, None),
61+ cx,
62+ )
63+ });
64+
65+ let session = task.await.unwrap();
66+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
67+
68+ client
69+ .on_request::<dap::requests::Threads, _>(move |_, _| {
70+ Ok(dap::ThreadsResponse {
71+ threads: vec![dap::Thread {
72+ id: 1,
73+ name: "Thread 1".into(),
74+ }],
75+ })
76+ })
77+ .await;
78+
79+ let stack_frames = vec![StackFrame {
80+ id: 1,
81+ name: "Stack Frame 1".into(),
82+ source: Some(dap::Source {
83+ name: Some("test.js".into()),
84+ path: Some(path!("/project/src/test.js").into()),
85+ source_reference: None,
86+ presentation_hint: None,
87+ origin: None,
88+ sources: None,
89+ adapter_data: None,
90+ checksums: None,
91+ }),
92+ line: 1,
93+ column: 1,
94+ end_line: None,
95+ end_column: None,
96+ can_restart: None,
97+ instruction_pointer_reference: None,
98+ module_id: None,
99+ presentation_hint: None,
100+ }];
101+
102+ client
103+ .on_request::<StackTrace, _>({
104+ let stack_frames = Arc::new(stack_frames.clone());
105+ move |_, args| {
106+ assert_eq!(1, args.thread_id);
107+
108+ Ok(dap::StackTraceResponse {
109+ stack_frames: (*stack_frames).clone(),
110+ total_frames: None,
111+ })
112+ }
113+ })
114+ .await;
115+
116+ let scopes = vec![Scope {
117+ name: "Scope 1".into(),
118+ presentation_hint: None,
119+ variables_reference: 2,
120+ named_variables: None,
121+ indexed_variables: None,
122+ expensive: false,
123+ source: None,
124+ line: None,
125+ column: None,
126+ end_line: None,
127+ end_column: None,
128+ }];
129+
130+ client
131+ .on_request::<Scopes, _>({
132+ let scopes = Arc::new(scopes.clone());
133+ move |_, args| {
134+ assert_eq!(1, args.frame_id);
135+
136+ Ok(dap::ScopesResponse {
137+ scopes: (*scopes).clone(),
138+ })
139+ }
140+ })
141+ .await;
142+
143+ let variables = vec![
144+ Variable {
145+ name: "variable1".into(),
146+ value: "value 1".into(),
147+ type_: None,
148+ presentation_hint: None,
149+ evaluate_name: None,
150+ variables_reference: 0,
151+ named_variables: None,
152+ indexed_variables: None,
153+ memory_reference: None,
154+ declaration_location_reference: None,
155+ value_location_reference: None,
156+ },
157+ Variable {
158+ name: "variable2".into(),
159+ value: "value 2".into(),
160+ type_: None,
161+ presentation_hint: None,
162+ evaluate_name: None,
163+ variables_reference: 0,
164+ named_variables: None,
165+ indexed_variables: None,
166+ memory_reference: None,
167+ declaration_location_reference: None,
168+ value_location_reference: None,
169+ },
170+ ];
171+
172+ client
173+ .on_request::<Variables, _>({
174+ let variables = Arc::new(variables.clone());
175+ move |_, args| {
176+ assert_eq!(2, args.variables_reference);
177+
178+ Ok(dap::VariablesResponse {
179+ variables: (*variables).clone(),
180+ })
181+ }
182+ })
183+ .await;
184+
185+ client
186+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
187+ reason: dap::StoppedEventReason::Pause,
188+ description: None,
189+ thread_id: Some(1),
190+ preserve_focus_hint: None,
191+ text: None,
192+ all_threads_stopped: None,
193+ hit_breakpoint_ids: None,
194+ }))
195+ .await;
196+
197+ cx.run_until_parked();
198+
199+ let running_state =
200+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
201+ cx.focus_self(window);
202+ item.mode()
203+ .as_running()
204+ .expect("Session should be running by this point")
205+ .clone()
206+ });
207+
208+ cx.run_until_parked();
209+
210+ running_state.update(cx, |running_state, cx| {
211+ let (stack_frame_list, stack_frame_id) =
212+ running_state.stack_frame_list().update(cx, |list, _| {
213+ (list.flatten_entries(), list.current_stack_frame_id())
214+ });
215+
216+ assert_eq!(stack_frames, stack_frame_list);
217+ assert_eq!(Some(1), stack_frame_id);
218+
219+ running_state
220+ .variable_list()
221+ .update(cx, |variable_list, _| {
222+ assert_eq!(1, variable_list.scopes().len());
223+ assert_eq!(scopes, variable_list.scopes());
224+ assert_eq!(
225+ vec![variables[0].clone(), variables[1].clone(),],
226+ variable_list.variables()
227+ );
228+
229+ variable_list.assert_visual_entries(vec![
230+ "v Scope 1",
231+ " > variable1",
232+ " > variable2",
233+ ]);
234+ });
235+ });
236+
237+ let shutdown_session = project.update(cx, |project, cx| {
238+ project.dap_store().update(cx, |dap_store, cx| {
239+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
240+ })
241+ });
242+
243+ shutdown_session.await.unwrap();
244+}
245+
246+/// This tests fetching multiple scopes and variables for them with a single stackframe
247+#[gpui::test]
248+async fn test_fetch_variables_for_multiple_scopes(
249+ executor: BackgroundExecutor,
250+ cx: &mut TestAppContext,
251+) {
252+ init_test(cx);
253+
254+ let fs = FakeFs::new(executor.clone());
255+
256+ let test_file_content = r#"
257+ const variable1 = {
258+ nested1: "Nested 1",
259+ nested2: "Nested 2",
260+ };
261+ const variable2 = "Value 2";
262+ const variable3 = "Value 3";
263+ "#
264+ .unindent();
265+
266+ fs.insert_tree(
267+ path!("/project"),
268+ json!({
269+ "src": {
270+ "test.js": test_file_content,
271+ }
272+ }),
273+ )
274+ .await;
275+
276+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
277+ let workspace = init_test_workspace(&project, cx).await;
278+ workspace
279+ .update(cx, |workspace, window, cx| {
280+ workspace.focus_panel::<DebugPanel>(window, cx);
281+ })
282+ .unwrap();
283+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
284+
285+ let task = project.update(cx, |project, cx| {
286+ project.start_debug_session(
287+ dap::test_config(dap::DebugRequestType::Launch, None, None),
288+ cx,
289+ )
290+ });
291+
292+ let session = task.await.unwrap();
293+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
294+
295+ client
296+ .on_request::<dap::requests::Threads, _>(move |_, _| {
297+ Ok(dap::ThreadsResponse {
298+ threads: vec![dap::Thread {
299+ id: 1,
300+ name: "Thread 1".into(),
301+ }],
302+ })
303+ })
304+ .await;
305+
306+ client
307+ .on_request::<Initialize, _>(move |_, _| {
308+ Ok(dap::Capabilities {
309+ supports_step_back: Some(false),
310+ ..Default::default()
311+ })
312+ })
313+ .await;
314+
315+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
316+
317+ let stack_frames = vec![StackFrame {
318+ id: 1,
319+ name: "Stack Frame 1".into(),
320+ source: Some(dap::Source {
321+ name: Some("test.js".into()),
322+ path: Some(path!("/project/src/test.js").into()),
323+ source_reference: None,
324+ presentation_hint: None,
325+ origin: None,
326+ sources: None,
327+ adapter_data: None,
328+ checksums: None,
329+ }),
330+ line: 1,
331+ column: 1,
332+ end_line: None,
333+ end_column: None,
334+ can_restart: None,
335+ instruction_pointer_reference: None,
336+ module_id: None,
337+ presentation_hint: None,
338+ }];
339+
340+ client
341+ .on_request::<StackTrace, _>({
342+ let stack_frames = Arc::new(stack_frames.clone());
343+ move |_, args| {
344+ assert_eq!(1, args.thread_id);
345+
346+ Ok(dap::StackTraceResponse {
347+ stack_frames: (*stack_frames).clone(),
348+ total_frames: None,
349+ })
350+ }
351+ })
352+ .await;
353+
354+ let scopes = vec![
355+ Scope {
356+ name: "Scope 1".into(),
357+ presentation_hint: Some(dap::ScopePresentationHint::Locals),
358+ variables_reference: 2,
359+ named_variables: None,
360+ indexed_variables: None,
361+ expensive: false,
362+ source: None,
363+ line: None,
364+ column: None,
365+ end_line: None,
366+ end_column: None,
367+ },
368+ Scope {
369+ name: "Scope 2".into(),
370+ presentation_hint: None,
371+ variables_reference: 3,
372+ named_variables: None,
373+ indexed_variables: None,
374+ expensive: false,
375+ source: None,
376+ line: None,
377+ column: None,
378+ end_line: None,
379+ end_column: None,
380+ },
381+ ];
382+
383+ client
384+ .on_request::<Scopes, _>({
385+ let scopes = Arc::new(scopes.clone());
386+ move |_, args| {
387+ assert_eq!(1, args.frame_id);
388+
389+ Ok(dap::ScopesResponse {
390+ scopes: (*scopes).clone(),
391+ })
392+ }
393+ })
394+ .await;
395+
396+ let mut variables = HashMap::default();
397+ variables.insert(
398+ 2,
399+ vec![
400+ Variable {
401+ name: "variable1".into(),
402+ value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(),
403+ type_: None,
404+ presentation_hint: None,
405+ evaluate_name: None,
406+ variables_reference: 0,
407+ named_variables: None,
408+ indexed_variables: None,
409+ memory_reference: None,
410+ declaration_location_reference: None,
411+ value_location_reference: None,
412+ },
413+ Variable {
414+ name: "variable2".into(),
415+ value: "Value 2".into(),
416+ type_: None,
417+ presentation_hint: None,
418+ evaluate_name: None,
419+ variables_reference: 0,
420+ named_variables: None,
421+ indexed_variables: None,
422+ memory_reference: None,
423+ declaration_location_reference: None,
424+ value_location_reference: None,
425+ },
426+ ],
427+ );
428+ variables.insert(
429+ 3,
430+ vec![Variable {
431+ name: "variable3".into(),
432+ value: "Value 3".into(),
433+ type_: None,
434+ presentation_hint: None,
435+ evaluate_name: None,
436+ variables_reference: 0,
437+ named_variables: None,
438+ indexed_variables: None,
439+ memory_reference: None,
440+ declaration_location_reference: None,
441+ value_location_reference: None,
442+ }],
443+ );
444+
445+ client
446+ .on_request::<Variables, _>({
447+ let variables = Arc::new(variables.clone());
448+ move |_, args| {
449+ Ok(dap::VariablesResponse {
450+ variables: variables.get(&args.variables_reference).unwrap().clone(),
451+ })
452+ }
453+ })
454+ .await;
455+
456+ client
457+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
458+ reason: dap::StoppedEventReason::Pause,
459+ description: None,
460+ thread_id: Some(1),
461+ preserve_focus_hint: None,
462+ text: None,
463+ all_threads_stopped: None,
464+ hit_breakpoint_ids: None,
465+ }))
466+ .await;
467+
468+ cx.run_until_parked();
469+
470+ let running_state =
471+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
472+ cx.focus_self(window);
473+ item.mode()
474+ .as_running()
475+ .expect("Session should be running by this point")
476+ .clone()
477+ });
478+
479+ cx.run_until_parked();
480+
481+ running_state.update(cx, |running_state, cx| {
482+ let (stack_frame_list, stack_frame_id) =
483+ running_state.stack_frame_list().update(cx, |list, _| {
484+ (list.flatten_entries(), list.current_stack_frame_id())
485+ });
486+
487+ assert_eq!(Some(1), stack_frame_id);
488+ assert_eq!(stack_frames, stack_frame_list);
489+
490+ running_state
491+ .variable_list()
492+ .update(cx, |variable_list, _| {
493+ assert_eq!(2, variable_list.scopes().len());
494+ assert_eq!(scopes, variable_list.scopes());
495+ let variables_by_scope = variable_list.variables_per_scope();
496+
497+ // scope 1
498+ assert_eq!(
499+ vec![
500+ variables.get(&2).unwrap()[0].clone(),
501+ variables.get(&2).unwrap()[1].clone(),
502+ ],
503+ variables_by_scope[0].1
504+ );
505+
506+ // scope 2
507+ let empty_vec: Vec<dap::Variable> = vec![];
508+ assert_eq!(empty_vec, variables_by_scope[1].1);
509+
510+ variable_list.assert_visual_entries(vec![
511+ "v Scope 1",
512+ " > variable1",
513+ " > variable2",
514+ "> Scope 2",
515+ ]);
516+ });
517+ });
518+
519+ let shutdown_session = project.update(cx, |project, cx| {
520+ project.dap_store().update(cx, |dap_store, cx| {
521+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
522+ })
523+ });
524+
525+ shutdown_session.await.unwrap();
526+}
527+
528+// tests that toggling a variable will fetch its children and shows it
529+#[gpui::test]
530+async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestAppContext) {
531+ init_test(cx);
532+
533+ let fs = FakeFs::new(executor.clone());
534+
535+ let test_file_content = r#"
536+ const variable1 = {
537+ nested1: "Nested 1",
538+ nested2: "Nested 2",
539+ };
540+ const variable2 = "Value 2";
541+ const variable3 = "Value 3";
542+ "#
543+ .unindent();
544+
545+ fs.insert_tree(
546+ path!("/project"),
547+ json!({
548+ "src": {
549+ "test.js": test_file_content,
550+ }
551+ }),
552+ )
553+ .await;
554+
555+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
556+ let workspace = init_test_workspace(&project, cx).await;
557+ workspace
558+ .update(cx, |workspace, window, cx| {
559+ workspace.focus_panel::<DebugPanel>(window, cx);
560+ })
561+ .unwrap();
562+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
563+
564+ let task = project.update(cx, |project, cx| {
565+ project.start_debug_session(
566+ dap::test_config(dap::DebugRequestType::Launch, None, None),
567+ cx,
568+ )
569+ });
570+
571+ let session = task.await.unwrap();
572+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
573+
574+ client
575+ .on_request::<dap::requests::Threads, _>(move |_, _| {
576+ Ok(dap::ThreadsResponse {
577+ threads: vec![dap::Thread {
578+ id: 1,
579+ name: "Thread 1".into(),
580+ }],
581+ })
582+ })
583+ .await;
584+
585+ client
586+ .on_request::<Initialize, _>(move |_, _| {
587+ Ok(dap::Capabilities {
588+ supports_step_back: Some(false),
589+ ..Default::default()
590+ })
591+ })
592+ .await;
593+
594+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
595+
596+ let stack_frames = vec![StackFrame {
597+ id: 1,
598+ name: "Stack Frame 1".into(),
599+ source: Some(dap::Source {
600+ name: Some("test.js".into()),
601+ path: Some(path!("/project/src/test.js").into()),
602+ source_reference: None,
603+ presentation_hint: None,
604+ origin: None,
605+ sources: None,
606+ adapter_data: None,
607+ checksums: None,
608+ }),
609+ line: 1,
610+ column: 1,
611+ end_line: None,
612+ end_column: None,
613+ can_restart: None,
614+ instruction_pointer_reference: None,
615+ module_id: None,
616+ presentation_hint: None,
617+ }];
618+
619+ client
620+ .on_request::<StackTrace, _>({
621+ let stack_frames = Arc::new(stack_frames.clone());
622+ move |_, args| {
623+ assert_eq!(1, args.thread_id);
624+
625+ Ok(dap::StackTraceResponse {
626+ stack_frames: (*stack_frames).clone(),
627+ total_frames: None,
628+ })
629+ }
630+ })
631+ .await;
632+
633+ let scopes = vec![
634+ Scope {
635+ name: "Scope 1".into(),
636+ presentation_hint: Some(dap::ScopePresentationHint::Locals),
637+ variables_reference: 2,
638+ named_variables: None,
639+ indexed_variables: None,
640+ expensive: false,
641+ source: None,
642+ line: None,
643+ column: None,
644+ end_line: None,
645+ end_column: None,
646+ },
647+ Scope {
648+ name: "Scope 2".into(),
649+ presentation_hint: None,
650+ variables_reference: 4,
651+ named_variables: None,
652+ indexed_variables: None,
653+ expensive: false,
654+ source: None,
655+ line: None,
656+ column: None,
657+ end_line: None,
658+ end_column: None,
659+ },
660+ ];
661+
662+ client
663+ .on_request::<Scopes, _>({
664+ let scopes = Arc::new(scopes.clone());
665+ move |_, args| {
666+ assert_eq!(1, args.frame_id);
667+
668+ Ok(dap::ScopesResponse {
669+ scopes: (*scopes).clone(),
670+ })
671+ }
672+ })
673+ .await;
674+
675+ let scope1_variables = vec![
676+ Variable {
677+ name: "variable1".into(),
678+ value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(),
679+ type_: None,
680+ presentation_hint: None,
681+ evaluate_name: None,
682+ variables_reference: 3,
683+ named_variables: None,
684+ indexed_variables: None,
685+ memory_reference: None,
686+ declaration_location_reference: None,
687+ value_location_reference: None,
688+ },
689+ Variable {
690+ name: "variable2".into(),
691+ value: "Value 2".into(),
692+ type_: None,
693+ presentation_hint: None,
694+ evaluate_name: None,
695+ variables_reference: 0,
696+ named_variables: None,
697+ indexed_variables: None,
698+ memory_reference: None,
699+ declaration_location_reference: None,
700+ value_location_reference: None,
701+ },
702+ ];
703+
704+ let nested_variables = vec![
705+ Variable {
706+ name: "nested1".into(),
707+ value: "Nested 1".into(),
708+ type_: None,
709+ presentation_hint: None,
710+ evaluate_name: None,
711+ variables_reference: 0,
712+ named_variables: None,
713+ indexed_variables: None,
714+ memory_reference: None,
715+ declaration_location_reference: None,
716+ value_location_reference: None,
717+ },
718+ Variable {
719+ name: "nested2".into(),
720+ value: "Nested 2".into(),
721+ type_: None,
722+ presentation_hint: None,
723+ evaluate_name: None,
724+ variables_reference: 0,
725+ named_variables: None,
726+ indexed_variables: None,
727+ memory_reference: None,
728+ declaration_location_reference: None,
729+ value_location_reference: None,
730+ },
731+ ];
732+
733+ let scope2_variables = vec![Variable {
734+ name: "variable3".into(),
735+ value: "Value 3".into(),
736+ type_: None,
737+ presentation_hint: None,
738+ evaluate_name: None,
739+ variables_reference: 0,
740+ named_variables: None,
741+ indexed_variables: None,
742+ memory_reference: None,
743+ declaration_location_reference: None,
744+ value_location_reference: None,
745+ }];
746+
747+ client
748+ .on_request::<Variables, _>({
749+ let scope1_variables = Arc::new(scope1_variables.clone());
750+ let nested_variables = Arc::new(nested_variables.clone());
751+ let scope2_variables = Arc::new(scope2_variables.clone());
752+ move |_, args| match args.variables_reference {
753+ 4 => Ok(dap::VariablesResponse {
754+ variables: (*scope2_variables).clone(),
755+ }),
756+ 3 => Ok(dap::VariablesResponse {
757+ variables: (*nested_variables).clone(),
758+ }),
759+ 2 => Ok(dap::VariablesResponse {
760+ variables: (*scope1_variables).clone(),
761+ }),
762+ id => unreachable!("unexpected variables reference {id}"),
763+ }
764+ })
765+ .await;
766+
767+ client
768+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
769+ reason: dap::StoppedEventReason::Pause,
770+ description: None,
771+ thread_id: Some(1),
772+ preserve_focus_hint: None,
773+ text: None,
774+ all_threads_stopped: None,
775+ hit_breakpoint_ids: None,
776+ }))
777+ .await;
778+
779+ cx.run_until_parked();
780+ let running_state =
781+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
782+ cx.focus_self(window);
783+ let running = item
784+ .mode()
785+ .as_running()
786+ .expect("Session should be running by this point")
787+ .clone();
788+
789+ let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
790+ variable_list.update(cx, |_, cx| cx.focus_self(window));
791+ running
792+ });
793+
794+ cx.dispatch_action(SelectFirst);
795+ cx.run_until_parked();
796+
797+ running_state.update(cx, |running_state, cx| {
798+ running_state
799+ .variable_list()
800+ .update(cx, |variable_list, _| {
801+ variable_list.assert_visual_entries(vec![
802+ "v Scope 1 <=== selected",
803+ " > variable1",
804+ " > variable2",
805+ "> Scope 2",
806+ ]);
807+ });
808+ });
809+
810+ cx.dispatch_action(SelectNext);
811+ cx.run_until_parked();
812+
813+ running_state.update(cx, |running_state, cx| {
814+ running_state
815+ .variable_list()
816+ .update(cx, |variable_list, _| {
817+ variable_list.assert_visual_entries(vec![
818+ "v Scope 1",
819+ " > variable1 <=== selected",
820+ " > variable2",
821+ "> Scope 2",
822+ ]);
823+ });
824+ });
825+
826+ // expand the nested variables of variable 1
827+ cx.dispatch_action(ExpandSelectedEntry);
828+ cx.run_until_parked();
829+ running_state.update(cx, |running_state, cx| {
830+ running_state
831+ .variable_list()
832+ .update(cx, |variable_list, _| {
833+ variable_list.assert_visual_entries(vec![
834+ "v Scope 1",
835+ " v variable1 <=== selected",
836+ " > nested1",
837+ " > nested2",
838+ " > variable2",
839+ "> Scope 2",
840+ ]);
841+ });
842+ });
843+
844+ // select the first nested variable of variable 1
845+ cx.dispatch_action(SelectNext);
846+ cx.run_until_parked();
847+ running_state.update(cx, |debug_panel_item, cx| {
848+ debug_panel_item
849+ .variable_list()
850+ .update(cx, |variable_list, _| {
851+ variable_list.assert_visual_entries(vec![
852+ "v Scope 1",
853+ " v variable1",
854+ " > nested1 <=== selected",
855+ " > nested2",
856+ " > variable2",
857+ "> Scope 2",
858+ ]);
859+ });
860+ });
861+
862+ // select the second nested variable of variable 1
863+ cx.dispatch_action(SelectNext);
864+ cx.run_until_parked();
865+ running_state.update(cx, |debug_panel_item, cx| {
866+ debug_panel_item
867+ .variable_list()
868+ .update(cx, |variable_list, _| {
869+ variable_list.assert_visual_entries(vec![
870+ "v Scope 1",
871+ " v variable1",
872+ " > nested1",
873+ " > nested2 <=== selected",
874+ " > variable2",
875+ "> Scope 2",
876+ ]);
877+ });
878+ });
879+
880+ // select variable 2 of scope 1
881+ cx.dispatch_action(SelectNext);
882+ cx.run_until_parked();
883+ running_state.update(cx, |debug_panel_item, cx| {
884+ debug_panel_item
885+ .variable_list()
886+ .update(cx, |variable_list, _| {
887+ variable_list.assert_visual_entries(vec![
888+ "v Scope 1",
889+ " v variable1",
890+ " > nested1",
891+ " > nested2",
892+ " > variable2 <=== selected",
893+ "> Scope 2",
894+ ]);
895+ });
896+ });
897+
898+ // select scope 2
899+ cx.dispatch_action(SelectNext);
900+ cx.run_until_parked();
901+ running_state.update(cx, |debug_panel_item, cx| {
902+ debug_panel_item
903+ .variable_list()
904+ .update(cx, |variable_list, _| {
905+ variable_list.assert_visual_entries(vec![
906+ "v Scope 1",
907+ " v variable1",
908+ " > nested1",
909+ " > nested2",
910+ " > variable2",
911+ "> Scope 2 <=== selected",
912+ ]);
913+ });
914+ });
915+
916+ // expand the nested variables of scope 2
917+ cx.dispatch_action(ExpandSelectedEntry);
918+ cx.run_until_parked();
919+ running_state.update(cx, |debug_panel_item, cx| {
920+ debug_panel_item
921+ .variable_list()
922+ .update(cx, |variable_list, _| {
923+ variable_list.assert_visual_entries(vec![
924+ "v Scope 1",
925+ " v variable1",
926+ " > nested1",
927+ " > nested2",
928+ " > variable2",
929+ "v Scope 2 <=== selected",
930+ " > variable3",
931+ ]);
932+ });
933+ });
934+
935+ // select variable 3 of scope 2
936+ cx.dispatch_action(SelectNext);
937+ cx.run_until_parked();
938+ running_state.update(cx, |debug_panel_item, cx| {
939+ debug_panel_item
940+ .variable_list()
941+ .update(cx, |variable_list, _| {
942+ variable_list.assert_visual_entries(vec![
943+ "v Scope 1",
944+ " v variable1",
945+ " > nested1",
946+ " > nested2",
947+ " > variable2",
948+ "v Scope 2",
949+ " > variable3 <=== selected",
950+ ]);
951+ });
952+ });
953+
954+ // select scope 2
955+ cx.dispatch_action(SelectPrevious);
956+ cx.run_until_parked();
957+ running_state.update(cx, |debug_panel_item, cx| {
958+ debug_panel_item
959+ .variable_list()
960+ .update(cx, |variable_list, _| {
961+ variable_list.assert_visual_entries(vec![
962+ "v Scope 1",
963+ " v variable1",
964+ " > nested1",
965+ " > nested2",
966+ " > variable2",
967+ "v Scope 2 <=== selected",
968+ " > variable3",
969+ ]);
970+ });
971+ });
972+
973+ // collapse variables of scope 2
974+ cx.dispatch_action(CollapseSelectedEntry);
975+ cx.run_until_parked();
976+ running_state.update(cx, |debug_panel_item, cx| {
977+ debug_panel_item
978+ .variable_list()
979+ .update(cx, |variable_list, _| {
980+ variable_list.assert_visual_entries(vec![
981+ "v Scope 1",
982+ " v variable1",
983+ " > nested1",
984+ " > nested2",
985+ " > variable2",
986+ "> Scope 2 <=== selected",
987+ ]);
988+ });
989+ });
990+
991+ // select variable 2 of scope 1
992+ cx.dispatch_action(SelectPrevious);
993+ cx.run_until_parked();
994+ running_state.update(cx, |debug_panel_item, cx| {
995+ debug_panel_item
996+ .variable_list()
997+ .update(cx, |variable_list, _| {
998+ variable_list.assert_visual_entries(vec![
999+ "v Scope 1",
1000+ " v variable1",
1001+ " > nested1",
1002+ " > nested2",
1003+ " > variable2 <=== selected",
1004+ "> Scope 2",
1005+ ]);
1006+ });
1007+ });
1008+
1009+ // select nested2 of variable 1
1010+ cx.dispatch_action(SelectPrevious);
1011+ cx.run_until_parked();
1012+ running_state.update(cx, |debug_panel_item, cx| {
1013+ debug_panel_item
1014+ .variable_list()
1015+ .update(cx, |variable_list, _| {
1016+ variable_list.assert_visual_entries(vec![
1017+ "v Scope 1",
1018+ " v variable1",
1019+ " > nested1",
1020+ " > nested2 <=== selected",
1021+ " > variable2",
1022+ "> Scope 2",
1023+ ]);
1024+ });
1025+ });
1026+
1027+ // select nested1 of variable 1
1028+ cx.dispatch_action(SelectPrevious);
1029+ cx.run_until_parked();
1030+ running_state.update(cx, |debug_panel_item, cx| {
1031+ debug_panel_item
1032+ .variable_list()
1033+ .update(cx, |variable_list, _| {
1034+ variable_list.assert_visual_entries(vec![
1035+ "v Scope 1",
1036+ " v variable1",
1037+ " > nested1 <=== selected",
1038+ " > nested2",
1039+ " > variable2",
1040+ "> Scope 2",
1041+ ]);
1042+ });
1043+ });
1044+
1045+ // select variable 1 of scope 1
1046+ cx.dispatch_action(SelectPrevious);
1047+ cx.run_until_parked();
1048+ running_state.update(cx, |debug_panel_item, cx| {
1049+ debug_panel_item
1050+ .variable_list()
1051+ .update(cx, |variable_list, _| {
1052+ variable_list.assert_visual_entries(vec![
1053+ "v Scope 1",
1054+ " v variable1 <=== selected",
1055+ " > nested1",
1056+ " > nested2",
1057+ " > variable2",
1058+ "> Scope 2",
1059+ ]);
1060+ });
1061+ });
1062+
1063+ // collapse variables of variable 1
1064+ cx.dispatch_action(CollapseSelectedEntry);
1065+ cx.run_until_parked();
1066+ running_state.update(cx, |debug_panel_item, cx| {
1067+ debug_panel_item
1068+ .variable_list()
1069+ .update(cx, |variable_list, _| {
1070+ variable_list.assert_visual_entries(vec![
1071+ "v Scope 1",
1072+ " > variable1 <=== selected",
1073+ " > variable2",
1074+ "> Scope 2",
1075+ ]);
1076+ });
1077+ });
1078+
1079+ // select scope 1
1080+ cx.dispatch_action(SelectPrevious);
1081+ cx.run_until_parked();
1082+ running_state.update(cx, |running_state, cx| {
1083+ running_state
1084+ .variable_list()
1085+ .update(cx, |variable_list, _| {
1086+ variable_list.assert_visual_entries(vec![
1087+ "v Scope 1 <=== selected",
1088+ " > variable1",
1089+ " > variable2",
1090+ "> Scope 2",
1091+ ]);
1092+ });
1093+ });
1094+
1095+ // collapse variables of scope 1
1096+ cx.dispatch_action(CollapseSelectedEntry);
1097+ cx.run_until_parked();
1098+ running_state.update(cx, |debug_panel_item, cx| {
1099+ debug_panel_item
1100+ .variable_list()
1101+ .update(cx, |variable_list, _| {
1102+ variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]);
1103+ });
1104+ });
1105+
1106+ let shutdown_session = project.update(cx, |project, cx| {
1107+ project.dap_store().update(cx, |dap_store, cx| {
1108+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
1109+ })
1110+ });
1111+
1112+ shutdown_session.await.unwrap();
1113+}
1114+
1115+#[gpui::test]
1116+async fn test_variable_list_only_sends_requests_when_rendering(
1117+ executor: BackgroundExecutor,
1118+ cx: &mut TestAppContext,
1119+) {
1120+ init_test(cx);
1121+
1122+ let fs = FakeFs::new(executor.clone());
1123+
1124+ let test_file_content = r#"
1125+ import { SOME_VALUE } './module.js';
1126+
1127+ console.log(SOME_VALUE);
1128+ "#
1129+ .unindent();
1130+
1131+ let module_file_content = r#"
1132+ export SOME_VALUE = 'some value';
1133+ "#
1134+ .unindent();
1135+
1136+ fs.insert_tree(
1137+ path!("/project"),
1138+ json!({
1139+ "src": {
1140+ "test.js": test_file_content,
1141+ "module.js": module_file_content,
1142+ }
1143+ }),
1144+ )
1145+ .await;
1146+
1147+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1148+ let workspace = init_test_workspace(&project, cx).await;
1149+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
1150+
1151+ let task = project.update(cx, |project, cx| {
1152+ project.start_debug_session(
1153+ dap::test_config(dap::DebugRequestType::Launch, None, None),
1154+ cx,
1155+ )
1156+ });
1157+
1158+ let session = task.await.unwrap();
1159+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1160+
1161+ client
1162+ .on_request::<dap::requests::Threads, _>(move |_, _| {
1163+ Ok(dap::ThreadsResponse {
1164+ threads: vec![dap::Thread {
1165+ id: 1,
1166+ name: "Thread 1".into(),
1167+ }],
1168+ })
1169+ })
1170+ .await;
1171+
1172+ client
1173+ .on_request::<Initialize, _>(move |_, _| {
1174+ Ok(dap::Capabilities {
1175+ supports_step_back: Some(false),
1176+ ..Default::default()
1177+ })
1178+ })
1179+ .await;
1180+
1181+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1182+
1183+ let stack_frames = vec![
1184+ StackFrame {
1185+ id: 1,
1186+ name: "Stack Frame 1".into(),
1187+ source: Some(dap::Source {
1188+ name: Some("test.js".into()),
1189+ path: Some(path!("/project/src/test.js").into()),
1190+ source_reference: None,
1191+ presentation_hint: None,
1192+ origin: None,
1193+ sources: None,
1194+ adapter_data: None,
1195+ checksums: None,
1196+ }),
1197+ line: 3,
1198+ column: 1,
1199+ end_line: None,
1200+ end_column: None,
1201+ can_restart: None,
1202+ instruction_pointer_reference: None,
1203+ module_id: None,
1204+ presentation_hint: None,
1205+ },
1206+ StackFrame {
1207+ id: 2,
1208+ name: "Stack Frame 2".into(),
1209+ source: Some(dap::Source {
1210+ name: Some("module.js".into()),
1211+ path: Some(path!("/project/src/module.js").into()),
1212+ source_reference: None,
1213+ presentation_hint: None,
1214+ origin: None,
1215+ sources: None,
1216+ adapter_data: None,
1217+ checksums: None,
1218+ }),
1219+ line: 1,
1220+ column: 1,
1221+ end_line: None,
1222+ end_column: None,
1223+ can_restart: None,
1224+ instruction_pointer_reference: None,
1225+ module_id: None,
1226+ presentation_hint: None,
1227+ },
1228+ ];
1229+
1230+ client
1231+ .on_request::<StackTrace, _>({
1232+ let stack_frames = Arc::new(stack_frames.clone());
1233+ move |_, args| {
1234+ assert_eq!(1, args.thread_id);
1235+
1236+ Ok(dap::StackTraceResponse {
1237+ stack_frames: (*stack_frames).clone(),
1238+ total_frames: None,
1239+ })
1240+ }
1241+ })
1242+ .await;
1243+
1244+ let frame_1_scopes = vec![Scope {
1245+ name: "Frame 1 Scope 1".into(),
1246+ presentation_hint: None,
1247+ variables_reference: 2,
1248+ named_variables: None,
1249+ indexed_variables: None,
1250+ expensive: false,
1251+ source: None,
1252+ line: None,
1253+ column: None,
1254+ end_line: None,
1255+ end_column: None,
1256+ }];
1257+
1258+ let made_scopes_request = Arc::new(AtomicBool::new(false));
1259+
1260+ client
1261+ .on_request::<Scopes, _>({
1262+ let frame_1_scopes = Arc::new(frame_1_scopes.clone());
1263+ let made_scopes_request = made_scopes_request.clone();
1264+ move |_, args| {
1265+ assert_eq!(1, args.frame_id);
1266+ assert!(
1267+ !made_scopes_request.load(Ordering::SeqCst),
1268+ "We should be caching the scope request"
1269+ );
1270+
1271+ made_scopes_request.store(true, Ordering::SeqCst);
1272+
1273+ Ok(dap::ScopesResponse {
1274+ scopes: (*frame_1_scopes).clone(),
1275+ })
1276+ }
1277+ })
1278+ .await;
1279+
1280+ let frame_1_variables = vec![
1281+ Variable {
1282+ name: "variable1".into(),
1283+ value: "value 1".into(),
1284+ type_: None,
1285+ presentation_hint: None,
1286+ evaluate_name: None,
1287+ variables_reference: 0,
1288+ named_variables: None,
1289+ indexed_variables: None,
1290+ memory_reference: None,
1291+ declaration_location_reference: None,
1292+ value_location_reference: None,
1293+ },
1294+ Variable {
1295+ name: "variable2".into(),
1296+ value: "value 2".into(),
1297+ type_: None,
1298+ presentation_hint: None,
1299+ evaluate_name: None,
1300+ variables_reference: 0,
1301+ named_variables: None,
1302+ indexed_variables: None,
1303+ memory_reference: None,
1304+ declaration_location_reference: None,
1305+ value_location_reference: None,
1306+ },
1307+ ];
1308+
1309+ client
1310+ .on_request::<Variables, _>({
1311+ let frame_1_variables = Arc::new(frame_1_variables.clone());
1312+ move |_, args| {
1313+ assert_eq!(2, args.variables_reference);
1314+
1315+ Ok(dap::VariablesResponse {
1316+ variables: (*frame_1_variables).clone(),
1317+ })
1318+ }
1319+ })
1320+ .await;
1321+
1322+ let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, cx| {
1323+ let state = item
1324+ .mode()
1325+ .as_running()
1326+ .expect("Session should be running by this point")
1327+ .clone();
1328+
1329+ state.update(cx, |state, cx| {
1330+ state.set_thread_item(crate::session::ThreadItem::Modules, cx)
1331+ });
1332+ state
1333+ });
1334+
1335+ client
1336+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1337+ reason: dap::StoppedEventReason::Pause,
1338+ description: None,
1339+ thread_id: Some(1),
1340+ preserve_focus_hint: None,
1341+ text: None,
1342+ all_threads_stopped: None,
1343+ hit_breakpoint_ids: None,
1344+ }))
1345+ .await;
1346+
1347+ cx.run_until_parked();
1348+
1349+ // We shouldn't make any variable requests unless we're rendering the variable list
1350+ running_state.update_in(cx, |running_state, window, cx| {
1351+ let variable_list = running_state.variable_list().read(cx);
1352+ let empty: Vec<dap::Variable> = vec![];
1353+
1354+ assert_eq!(empty, variable_list.variables());
1355+ assert!(!made_scopes_request.load(Ordering::SeqCst));
1356+
1357+ cx.focus_self(window);
1358+ running_state.set_thread_item(crate::session::ThreadItem::Variables, cx);
1359+ });
1360+
1361+ cx.run_until_parked();
1362+
1363+ running_state.update(cx, |running_state, cx| {
1364+ let (stack_frame_list, stack_frame_id) =
1365+ running_state.stack_frame_list().update(cx, |list, _| {
1366+ (list.flatten_entries(), list.current_stack_frame_id())
1367+ });
1368+
1369+ assert_eq!(Some(1), stack_frame_id);
1370+ assert_eq!(stack_frames, stack_frame_list);
1371+
1372+ let variable_list = running_state.variable_list().read(cx);
1373+
1374+ assert_eq!(frame_1_variables, variable_list.variables());
1375+ assert!(made_scopes_request.load(Ordering::SeqCst));
1376+ });
1377+
1378+ let shutdown_session = project.update(cx, |project, cx| {
1379+ project.dap_store().update(cx, |dap_store, cx| {
1380+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
1381+ })
1382+ });
1383+
1384+ shutdown_session.await.unwrap();
1385+}
1386+
1387+#[gpui::test]
1388+async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
1389+ executor: BackgroundExecutor,
1390+ cx: &mut TestAppContext,
1391+) {
1392+ init_test(cx);
1393+
1394+ let fs = FakeFs::new(executor.clone());
1395+
1396+ let test_file_content = r#"
1397+ import { SOME_VALUE } './module.js';
1398+
1399+ console.log(SOME_VALUE);
1400+ "#
1401+ .unindent();
1402+
1403+ let module_file_content = r#"
1404+ export SOME_VALUE = 'some value';
1405+ "#
1406+ .unindent();
1407+
1408+ fs.insert_tree(
1409+ path!("/project"),
1410+ json!({
1411+ "src": {
1412+ "test.js": test_file_content,
1413+ "module.js": module_file_content,
1414+ }
1415+ }),
1416+ )
1417+ .await;
1418+
1419+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1420+ let workspace = init_test_workspace(&project, cx).await;
1421+ workspace
1422+ .update(cx, |workspace, window, cx| {
1423+ workspace.focus_panel::<DebugPanel>(window, cx);
1424+ })
1425+ .unwrap();
1426+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
1427+
1428+ let task = project.update(cx, |project, cx| {
1429+ project.start_debug_session(
1430+ dap::test_config(dap::DebugRequestType::Launch, None, None),
1431+ cx,
1432+ )
1433+ });
1434+
1435+ let session = task.await.unwrap();
1436+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1437+
1438+ client
1439+ .on_request::<dap::requests::Threads, _>(move |_, _| {
1440+ Ok(dap::ThreadsResponse {
1441+ threads: vec![dap::Thread {
1442+ id: 1,
1443+ name: "Thread 1".into(),
1444+ }],
1445+ })
1446+ })
1447+ .await;
1448+
1449+ client
1450+ .on_request::<Initialize, _>(move |_, _| {
1451+ Ok(dap::Capabilities {
1452+ supports_step_back: Some(false),
1453+ ..Default::default()
1454+ })
1455+ })
1456+ .await;
1457+
1458+ client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1459+
1460+ let stack_frames = vec![
1461+ StackFrame {
1462+ id: 1,
1463+ name: "Stack Frame 1".into(),
1464+ source: Some(dap::Source {
1465+ name: Some("test.js".into()),
1466+ path: Some(path!("/project/src/test.js").into()),
1467+ source_reference: None,
1468+ presentation_hint: None,
1469+ origin: None,
1470+ sources: None,
1471+ adapter_data: None,
1472+ checksums: None,
1473+ }),
1474+ line: 3,
1475+ column: 1,
1476+ end_line: None,
1477+ end_column: None,
1478+ can_restart: None,
1479+ instruction_pointer_reference: None,
1480+ module_id: None,
1481+ presentation_hint: None,
1482+ },
1483+ StackFrame {
1484+ id: 2,
1485+ name: "Stack Frame 2".into(),
1486+ source: Some(dap::Source {
1487+ name: Some("module.js".into()),
1488+ path: Some(path!("/project/src/module.js").into()),
1489+ source_reference: None,
1490+ presentation_hint: None,
1491+ origin: None,
1492+ sources: None,
1493+ adapter_data: None,
1494+ checksums: None,
1495+ }),
1496+ line: 1,
1497+ column: 1,
1498+ end_line: None,
1499+ end_column: None,
1500+ can_restart: None,
1501+ instruction_pointer_reference: None,
1502+ module_id: None,
1503+ presentation_hint: None,
1504+ },
1505+ ];
1506+
1507+ client
1508+ .on_request::<StackTrace, _>({
1509+ let stack_frames = Arc::new(stack_frames.clone());
1510+ move |_, args| {
1511+ assert_eq!(1, args.thread_id);
1512+
1513+ Ok(dap::StackTraceResponse {
1514+ stack_frames: (*stack_frames).clone(),
1515+ total_frames: None,
1516+ })
1517+ }
1518+ })
1519+ .await;
1520+
1521+ let frame_1_scopes = vec![Scope {
1522+ name: "Frame 1 Scope 1".into(),
1523+ presentation_hint: None,
1524+ variables_reference: 2,
1525+ named_variables: None,
1526+ indexed_variables: None,
1527+ expensive: false,
1528+ source: None,
1529+ line: None,
1530+ column: None,
1531+ end_line: None,
1532+ end_column: None,
1533+ }];
1534+
1535+ // add handlers for fetching the second stack frame's scopes and variables
1536+ // after the user clicked the stack frame
1537+ let frame_2_scopes = vec![Scope {
1538+ name: "Frame 2 Scope 1".into(),
1539+ presentation_hint: None,
1540+ variables_reference: 3,
1541+ named_variables: None,
1542+ indexed_variables: None,
1543+ expensive: false,
1544+ source: None,
1545+ line: None,
1546+ column: None,
1547+ end_line: None,
1548+ end_column: None,
1549+ }];
1550+
1551+ let called_second_stack_frame = Arc::new(AtomicBool::new(false));
1552+ let called_first_stack_frame = Arc::new(AtomicBool::new(false));
1553+
1554+ client
1555+ .on_request::<Scopes, _>({
1556+ let frame_1_scopes = Arc::new(frame_1_scopes.clone());
1557+ let frame_2_scopes = Arc::new(frame_2_scopes.clone());
1558+ let called_first_stack_frame = called_first_stack_frame.clone();
1559+ let called_second_stack_frame = called_second_stack_frame.clone();
1560+ move |_, args| match args.frame_id {
1561+ 1 => {
1562+ called_first_stack_frame.store(true, Ordering::SeqCst);
1563+ Ok(dap::ScopesResponse {
1564+ scopes: (*frame_1_scopes).clone(),
1565+ })
1566+ }
1567+ 2 => {
1568+ called_second_stack_frame.store(true, Ordering::SeqCst);
1569+
1570+ Ok(dap::ScopesResponse {
1571+ scopes: (*frame_2_scopes).clone(),
1572+ })
1573+ }
1574+ _ => panic!("Made a scopes request with an invalid frame id"),
1575+ }
1576+ })
1577+ .await;
1578+
1579+ let frame_1_variables = vec![
1580+ Variable {
1581+ name: "variable1".into(),
1582+ value: "value 1".into(),
1583+ type_: None,
1584+ presentation_hint: None,
1585+ evaluate_name: None,
1586+ variables_reference: 0,
1587+ named_variables: None,
1588+ indexed_variables: None,
1589+ memory_reference: None,
1590+ declaration_location_reference: None,
1591+ value_location_reference: None,
1592+ },
1593+ Variable {
1594+ name: "variable2".into(),
1595+ value: "value 2".into(),
1596+ type_: None,
1597+ presentation_hint: None,
1598+ evaluate_name: None,
1599+ variables_reference: 0,
1600+ named_variables: None,
1601+ indexed_variables: None,
1602+ memory_reference: None,
1603+ declaration_location_reference: None,
1604+ value_location_reference: None,
1605+ },
1606+ ];
1607+
1608+ let frame_2_variables = vec![
1609+ Variable {
1610+ name: "variable3".into(),
1611+ value: "old value 1".into(),
1612+ type_: None,
1613+ presentation_hint: None,
1614+ evaluate_name: None,
1615+ variables_reference: 0,
1616+ named_variables: None,
1617+ indexed_variables: None,
1618+ memory_reference: None,
1619+ declaration_location_reference: None,
1620+ value_location_reference: None,
1621+ },
1622+ Variable {
1623+ name: "variable4".into(),
1624+ value: "old value 2".into(),
1625+ type_: None,
1626+ presentation_hint: None,
1627+ evaluate_name: None,
1628+ variables_reference: 0,
1629+ named_variables: None,
1630+ indexed_variables: None,
1631+ memory_reference: None,
1632+ declaration_location_reference: None,
1633+ value_location_reference: None,
1634+ },
1635+ ];
1636+
1637+ client
1638+ .on_request::<Variables, _>({
1639+ let frame_1_variables = Arc::new(frame_1_variables.clone());
1640+ move |_, args| {
1641+ assert_eq!(2, args.variables_reference);
1642+
1643+ Ok(dap::VariablesResponse {
1644+ variables: (*frame_1_variables).clone(),
1645+ })
1646+ }
1647+ })
1648+ .await;
1649+
1650+ client
1651+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1652+ reason: dap::StoppedEventReason::Pause,
1653+ description: None,
1654+ thread_id: Some(1),
1655+ preserve_focus_hint: None,
1656+ text: None,
1657+ all_threads_stopped: None,
1658+ hit_breakpoint_ids: None,
1659+ }))
1660+ .await;
1661+
1662+ cx.run_until_parked();
1663+
1664+ let running_state =
1665+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
1666+ cx.focus_self(window);
1667+ item.mode()
1668+ .as_running()
1669+ .expect("Session should be running by this point")
1670+ .clone()
1671+ });
1672+
1673+ cx.run_until_parked();
1674+
1675+ running_state.update(cx, |running_state, cx| {
1676+ let (stack_frame_list, stack_frame_id) =
1677+ running_state.stack_frame_list().update(cx, |list, _| {
1678+ (list.flatten_entries(), list.current_stack_frame_id())
1679+ });
1680+
1681+ let variable_list = running_state.variable_list().read(cx);
1682+ let variables = variable_list.variables();
1683+
1684+ assert_eq!(Some(1), stack_frame_id);
1685+ assert_eq!(
1686+ running_state
1687+ .stack_frame_list()
1688+ .read(cx)
1689+ .current_stack_frame_id(),
1690+ Some(1)
1691+ );
1692+
1693+ assert!(
1694+ called_first_stack_frame.load(std::sync::atomic::Ordering::SeqCst),
1695+ "Request scopes shouldn't be called before it's needed"
1696+ );
1697+ assert!(
1698+ !called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst),
1699+ "Request scopes shouldn't be called before it's needed"
1700+ );
1701+
1702+ assert_eq!(stack_frames, stack_frame_list);
1703+ assert_eq!(frame_1_variables, variables);
1704+ });
1705+
1706+ client
1707+ .on_request::<Variables, _>({
1708+ let frame_2_variables = Arc::new(frame_2_variables.clone());
1709+ move |_, args| {
1710+ assert_eq!(3, args.variables_reference);
1711+
1712+ Ok(dap::VariablesResponse {
1713+ variables: (*frame_2_variables).clone(),
1714+ })
1715+ }
1716+ })
1717+ .await;
1718+
1719+ running_state
1720+ .update_in(cx, |running_state, window, cx| {
1721+ running_state
1722+ .stack_frame_list()
1723+ .update(cx, |stack_frame_list, cx| {
1724+ stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
1725+ })
1726+ })
1727+ .await
1728+ .unwrap();
1729+
1730+ cx.run_until_parked();
1731+
1732+ running_state.update(cx, |running_state, cx| {
1733+ let (stack_frame_list, stack_frame_id) =
1734+ running_state.stack_frame_list().update(cx, |list, _| {
1735+ (list.flatten_entries(), list.current_stack_frame_id())
1736+ });
1737+
1738+ let variable_list = running_state.variable_list().read(cx);
1739+ let variables = variable_list.variables();
1740+
1741+ assert_eq!(Some(2), stack_frame_id);
1742+ assert!(
1743+ called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst),
1744+ "Request scopes shouldn't be called before it's needed"
1745+ );
1746+
1747+ assert_eq!(stack_frames, stack_frame_list);
1748+
1749+ assert_eq!(variables, frame_2_variables,);
1750+ });
1751+
1752+ let shutdown_session = project.update(cx, |project, cx| {
1753+ project.dap_store().update(cx, |dap_store, cx| {
1754+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
1755+ })
1756+ });
1757+
1758+ shutdown_session.await.unwrap();
1759+}
crates/editor/Cargo.toml+2−0
@@ -40,6 +40,7 @@ convert_case.workspace = true · 39 unmodified lines
4040 db.workspace = true
4141 buffer_diff.workspace = true
4242 emojis.workspace = true
43+feature_flags.workspace = true
4344 file_icons.workspace = true
4445 futures.workspace = true
4546 fuzzy.workspace = true
@@ -55,6 +56,7 @@ linkify.workspace = true · 9 unmodified lines
5556 log.workspace = true
5657 lsp.workspace = true
5758 markdown.workspace = true
59+menu.workspace = true
5860 multi_buffer.workspace = true
5961 ordered-float.workspace = true
6062 parking_lot.workspace = true
crates/editor/src/actions.rs+2−0
@@ -412,6 +412,8 @@ actions!( · 411 unmodified lines
412412 SwitchSourceHeader,
413413 Tab,
414414 Backtab,
415+ ToggleBreakpoint,
416+ EditLogBreakpoint,
415417 ToggleAutoSignatureHelp,
416418 ToggleGitBlameInline,
417419 ToggleIndentGuides,
crates/editor/src/editor.rs+761−7
@@ -68,6 +68,7 @@ use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, Posi · 67 unmodified lines
6868 pub use element::{
6969 CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
7070 };
71+use feature_flags::{Debugger, FeatureFlagAppExt};
7172 use futures::{
7273 future::{self, join, Shared},
7374 FutureExt,
@@ -83,7 +84,7 @@ use git::blame::GitBlame; · 9 unmodified lines
8384 use gpui::{
8485 div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
8586 AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background,
86− Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
87+ Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
8788 EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,
8889 Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
8990 ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
@@ -91,6 +92,7 @@ use gpui::{ · 1 unmodified line
9192 WeakEntity, WeakFocusHandle, Window,
9293 };
9394 use highlight_matching_bracket::refresh_matching_bracket_highlights;
95+use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
9496 use hover_popover::{hide_hover, HoverState};
9597 use indent_guides::ActiveIndentGuidesState;
9698 use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
@@ -112,6 +114,11 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} · 15 unmodified lines
112114 use linked_editing_ranges::refresh_linked_ranges;
113115 use mouse_context_menu::MouseContextMenu;
114116 use persistence::DB;
117+use project::{
118+ debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent},
119+ ProjectPath,
120+};
121+
115122 pub use proposed_changes_editor::{
116123 ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
117124 };
@@ -119,7 +126,6 @@ use smallvec::smallvec; · 1 unmodified line
119126 use std::iter::Peekable;
120127 use task::{ResolvedTask, TaskTemplate, TaskVariables};
121128
122−use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
123129 pub use lsp::CompletionContext;
124130 use lsp::{
125131 CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
@@ -136,7 +142,9 @@ use multi_buffer::{ · 10 unmodified lines
136142 ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
137143 MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
138144 };
145+use parking_lot::Mutex;
139146 use project::{
147+ debugger::breakpoint_store::{Breakpoint, BreakpointKind},
140148 lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
141149 project_settings::{GitGutterSetting, ProjectSettings},
142150 CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
@@ -153,6 +161,7 @@ use serde::{Deserialize, Serialize}; · 10 unmodified lines
153161 use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
154162 use smallvec::SmallVec;
155163 use snippet::Snippet;
164+use std::sync::Arc;
156165 use std::{
157166 any::TypeId,
158167 borrow::Cow,
@@ -163,7 +172,6 @@ use std::{ · 4 unmodified lines
163172 ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
164173 path::{Path, PathBuf},
165174 rc::Rc,
166− sync::Arc,
167175 time::{Duration, Instant},
168176 };
169177 pub use sum_tree::Bias;
@@ -236,6 +244,7 @@ impl InlayId { · 66 unmodified lines
236244 }
237245 }
238246
247+pub enum DebugCurrentRowHighlight {}
239248 enum DocumentHighlightRead {}
240249 enum DocumentHighlightWrite {}
241250 enum InputComposition {}
@@ -589,6 +598,7 @@ struct ResolvedTasks { · 347 unmodified lines
589598 templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
590599 position: Anchor,
591600 }
601+
592602 #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
593603 struct BufferOffset(usize);
594604
@@ -659,6 +669,7 @@ pub struct Editor { · 64 unmodified lines
659669 show_git_diff_gutter: Option<bool>,
660670 show_code_actions: Option<bool>,
661671 show_runnables: Option<bool>,
672+ show_breakpoints: Option<bool>,
662673 show_wrap_guides: Option<bool>,
663674 show_indent_guides: Option<bool>,
664675 placeholder_text: Option<Arc<str>>,
@@ -749,6 +760,11 @@ pub struct Editor { · 84 unmodified lines
749760 expect_bounds_change: Option<Bounds<Pixels>>,
750761 tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
751762 tasks_update_task: Option<Task<()>>,
763+ pub breakpoint_store: Option<Entity<BreakpointStore>>,
764+ /// Allow's a user to create a breakpoint by selecting this indicator
765+ /// It should be None while a user is not hovering over the gutter
766+ /// Otherwise it represents the point that the breakpoint will be shown
767+ pub gutter_breakpoint_indicator: Option<DisplayPoint>,
752768 in_project_search: bool,
753769 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
754770 breadcrumb_header: Option<String>,
@@ -789,6 +805,7 @@ pub struct EditorSnapshot { · 34 unmodified lines
789805 show_git_diff_gutter: Option<bool>,
790806 show_code_actions: Option<bool>,
791807 show_runnables: Option<bool>,
808+ show_breakpoints: Option<bool>,
792809 git_blame_gutter_max_author_length: Option<usize>,
793810 pub display_snapshot: DisplaySnapshot,
794811 pub placeholder_text: Option<Arc<str>>,
@@ -1279,7 +1296,18 @@ impl Editor { · 484 unmodified lines
12791296 editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
12801297 },
12811298 ));
1282− }
1299+ };
1300+
1301+ project_subscriptions.push(cx.subscribe_in(
1302+ &project.read(cx).breakpoint_store(),
1303+ window,
1304+ |editor, _, event, window, cx| match event {
1305+ BreakpointStoreEvent::ActiveDebugLineChanged => {
1306+ editor.go_to_active_debug_line(window, cx);
1307+ }
1308+ _ => {}
1309+ },
1310+ ));
12831311 }
12841312 }
12851313
@@ -1303,6 +1331,11 @@ impl Editor { · 17 unmodified lines
13031331 None
13041332 };
13051333
1334+ let breakpoint_store = match (mode, project.as_ref()) {
1335+ (EditorMode::Full, Some(project)) => Some(project.read(cx).breakpoint_store()),
1336+ _ => None,
1337+ };
1338+
13061339 let mut code_action_providers = Vec::new();
13071340 let mut load_uncommitted_diff = None;
13081341 if let Some(project) = project.clone() {
@@ -1356,6 +1389,7 @@ impl Editor { · 47 unmodified lines
13561389 show_git_diff_gutter: None,
13571390 show_code_actions: None,
13581391 show_runnables: None,
1392+ show_breakpoints: None,
13591393 show_wrap_guides: None,
13601394 show_indent_guides,
13611395 placeholder_text: None,
@@ -1440,6 +1474,9 @@ impl Editor { · 78 unmodified lines
14401474 blame: None,
14411475 blame_subscription: None,
14421476 tasks: Default::default(),
1477+
1478+ breakpoint_store,
1479+ gutter_breakpoint_indicator: None,
14431480 _subscriptions: vec![
14441481 cx.observe(&buffer, Self::on_buffer_changed),
14451482 cx.subscribe_in(&buffer, window, Self::on_buffer_event),
@@ -1474,6 +1511,12 @@ impl Editor { · 28 unmodified lines
14741511 text_style_refinement: None,
14751512 load_diff_task: load_uncommitted_diff,
14761513 };
1514+ if let Some(breakpoints) = this.breakpoint_store.as_ref() {
1515+ this._subscriptions
1516+ .push(cx.observe(breakpoints, |_, _, cx| {
1517+ cx.notify();
1518+ }));
1519+ }
14771520 this.tasks_update_task = Some(this.refresh_runnables(window, cx));
14781521 this._subscriptions.extend(project_subscriptions);
14791522
@@ -1490,6 +1533,8 @@ impl Editor { · 10 unmodified lines
14901533 this.start_git_blame_inline(false, window, cx);
14911534 }
14921535
1536+ this.go_to_active_debug_line(window, cx);
1537+
14931538 if let Some(buffer) = buffer.read(cx).as_singleton() {
14941539 if let Some(project) = this.project.as_ref() {
14951540 let handle = project.update(cx, |project, cx| {
@@ -1764,6 +1809,7 @@ impl Editor { · 268 unmodified lines
17641809 show_git_diff_gutter: self.show_git_diff_gutter,
17651810 show_code_actions: self.show_code_actions,
17661811 show_runnables: self.show_runnables,
1812+ show_breakpoints: self.show_breakpoints,
17671813 git_blame_gutter_max_author_length,
17681814 display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
17691815 scroll_anchor: self.scroll_manager.anchor(),
@@ -5857,14 +5903,28 @@ impl Editor { · 4087 unmodified lines
58575903 _style: &EditorStyle,
58585904 row: DisplayRow,
58595905 is_active: bool,
5906+ breakpoint: Option<&(Anchor, Breakpoint)>,
58605907 cx: &mut Context<Self>,
58615908 ) -> Option<IconButton> {
5909+ let color = if breakpoint.is_some() {
5910+ Color::Debugger
5911+ } else {
5912+ Color::Muted
5913+ };
5914+
5915+ let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
5916+ let bp_kind = Arc::new(
5917+ breakpoint
5918+ .map(|(_, bp)| bp.kind.clone())
5919+ .unwrap_or(BreakpointKind::Standard),
5920+ );
5921+
58625922 if self.available_code_actions.is_some() {
58635923 Some(
58645924 IconButton::new("code_actions_indicator", ui::IconName::Bolt)
58655925 .shape(ui::IconButtonShape::Square)
58665926 .icon_size(IconSize::XSmall)
5867− .icon_color(Color::Muted)
5927+ .icon_color(color)
58685928 .toggle_state(is_active)
58695929 .tooltip({
58705930 let focus_handle = self.focus_handle.clone();
@@ -5889,6 +5949,16 @@ impl Editor { · 18 unmodified lines
58895949 window,
58905950 cx,
58915951 );
5952+ }))
5953+ .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
5954+ editor.set_breakpoint_context_menu(
5955+ row,
5956+ position,
5957+ bp_kind.clone(),
5958+ event.down.position,
5959+ window,
5960+ cx,
5961+ );
58925962 })),
58935963 )
58945964 } else {
@@ -5907,6 +5977,198 @@ impl Editor { · 12 unmodified lines
59075977 }
59085978 }
59095979
5980+ /// Get all display points of breakpoints that will be rendered within editor
5981+ ///
5982+ /// This function is used to handle overlaps between breakpoints and Code action/runner symbol.
5983+ /// It's also used to set the color of line numbers with breakpoints to the breakpoint color.
5984+ /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints
5985+ fn active_breakpoints(
5986+ &mut self,
5987+ range: Range<DisplayRow>,
5988+ window: &mut Window,
5989+ cx: &mut Context<Self>,
5990+ ) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
5991+ let mut breakpoint_display_points = HashMap::default();
5992+
5993+ let Some(breakpoint_store) = self.breakpoint_store.clone() else {
5994+ return breakpoint_display_points;
5995+ };
5996+
5997+ let snapshot = self.snapshot(window, cx);
5998+
5999+ let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot;
6000+ let Some(project) = self.project.as_ref() else {
6001+ return breakpoint_display_points;
6002+ };
6003+
6004+ if let Some(buffer) = self.buffer.read(cx).as_singleton() {
6005+ let buffer_snapshot = buffer.read(cx).snapshot();
6006+
6007+ for breakpoint in
6008+ breakpoint_store
6009+ .read(cx)
6010+ .breakpoints(&buffer, None, buffer_snapshot.clone(), cx)
6011+ {
6012+ let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
6013+ let anchor = multi_buffer_snapshot.anchor_before(point);
6014+ breakpoint_display_points.insert(
6015+ snapshot
6016+ .point_to_display_point(
6017+ MultiBufferPoint {
6018+ row: point.row,
6019+ column: point.column,
6020+ },
6021+ Bias::Left,
6022+ )
6023+ .row(),
6024+ (anchor, breakpoint.1.clone()),
6025+ );
6026+ }
6027+
6028+ return breakpoint_display_points;
6029+ }
6030+
6031+ let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
6032+ ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
6033+ for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(range) {
6034+ let info = excerpt_boundary.next;
6035+
6036+ let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else {
6037+ continue;
6038+ };
6039+
6040+ let Some(buffer) =
6041+ project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx))
6042+ else {
6043+ continue;
6044+ };
6045+
6046+ let breakpoints = breakpoint_store.read(cx).breakpoints(
6047+ &buffer,
6048+ Some(info.range.context.start..info.range.context.end),
6049+ info.buffer.clone(),
6050+ cx,
6051+ );
6052+
6053+ // To translate a breakpoint's position within a singular buffer to a multi buffer
6054+ // position we need to know it's excerpt starting location, it's position within
6055+ // the singular buffer, and if that position is within the excerpt's range.
6056+ let excerpt_head = excerpt_ranges
6057+ .start
6058+ .to_display_point(&snapshot.display_snapshot);
6059+
6060+ let buffer_start = info
6061+ .buffer
6062+ .summary_for_anchor::<Point>(&info.range.context.start);
6063+
6064+ for (anchor, breakpoint) in breakpoints {
6065+ let as_row = info.buffer.summary_for_anchor::<Point>(&anchor).row;
6066+ let delta = as_row - buffer_start.row;
6067+
6068+ let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0);
6069+
6070+ let anchor = snapshot.display_point_to_anchor(position, Bias::Left);
6071+
6072+ breakpoint_display_points.insert(position.row(), (anchor, breakpoint.clone()));
6073+ }
6074+ }
6075+
6076+ breakpoint_display_points
6077+ }
6078+
6079+ fn breakpoint_context_menu(
6080+ &self,
6081+ anchor: Anchor,
6082+ kind: Arc<BreakpointKind>,
6083+ window: &mut Window,
6084+ cx: &mut Context<Self>,
6085+ ) -> Entity<ui::ContextMenu> {
6086+ let weak_editor = cx.weak_entity();
6087+ let focus_handle = self.focus_handle(cx);
6088+
6089+ let second_entry_msg = if kind.log_message().is_some() {
6090+ "Edit Log Breakpoint"
6091+ } else {
6092+ "Add Log Breakpoint"
6093+ };
6094+
6095+ ui::ContextMenu::build(window, cx, |menu, _, _cx| {
6096+ menu.on_blur_subscription(Subscription::new(|| {}))
6097+ .context(focus_handle)
6098+ .entry("Toggle Breakpoint", None, {
6099+ let weak_editor = weak_editor.clone();
6100+ move |_window, cx| {
6101+ weak_editor
6102+ .update(cx, |this, cx| {
6103+ this.edit_breakpoint_at_anchor(
6104+ anchor,
6105+ BreakpointKind::Standard,
6106+ BreakpointEditAction::Toggle,
6107+ cx,
6108+ );
6109+ })
6110+ .log_err();
6111+ }
6112+ })
6113+ .entry(second_entry_msg, None, move |window, cx| {
6114+ weak_editor
6115+ .update(cx, |this, cx| {
6116+ this.add_edit_breakpoint_block(anchor, kind.as_ref(), window, cx);
6117+ })
6118+ .log_err();
6119+ })
6120+ })
6121+ }
6122+
6123+ fn render_breakpoint(
6124+ &self,
6125+ position: Anchor,
6126+ row: DisplayRow,
6127+ kind: &BreakpointKind,
6128+ cx: &mut Context<Self>,
6129+ ) -> IconButton {
6130+ let color = if self
6131+ .gutter_breakpoint_indicator
6132+ .is_some_and(|gutter_bp| gutter_bp.row() == row)
6133+ {
6134+ Color::Hint
6135+ } else {
6136+ Color::Debugger
6137+ };
6138+
6139+ let icon = match &kind {
6140+ BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
6141+ BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
6142+ };
6143+ let arc_kind = Arc::new(kind.clone());
6144+ let arc_kind2 = arc_kind.clone();
6145+
6146+ IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
6147+ .icon_size(IconSize::XSmall)
6148+ .size(ui::ButtonSize::None)
6149+ .icon_color(color)
6150+ .style(ButtonStyle::Transparent)
6151+ .on_click(cx.listener(move |editor, _e, window, cx| {
6152+ window.focus(&editor.focus_handle(cx));
6153+ editor.edit_breakpoint_at_anchor(
6154+ position,
6155+ arc_kind.as_ref().clone(),
6156+ BreakpointEditAction::Toggle,
6157+ cx,
6158+ );
6159+ }))
6160+ .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
6161+ editor.set_breakpoint_context_menu(
6162+ row,
6163+ Some(position),
6164+ arc_kind2.clone(),
6165+ event.down.position,
6166+ window,
6167+ cx,
6168+ );
6169+ }))
6170+ }
6171+
59106172 fn build_tasks_context(
59116173 project: &Entity<Project>,
59126174 buffer: &Entity<Buffer>,
@@ -6043,12 +6305,26 @@ impl Editor { · 130 unmodified lines
60436305 _style: &EditorStyle,
60446306 is_active: bool,
60456307 row: DisplayRow,
6308+ breakpoint: Option<(Anchor, Breakpoint)>,
60466309 cx: &mut Context<Self>,
60476310 ) -> IconButton {
6311+ let color = if breakpoint.is_some() {
6312+ Color::Debugger
6313+ } else {
6314+ Color::Muted
6315+ };
6316+
6317+ let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
6318+ let bp_kind = Arc::new(
6319+ breakpoint
6320+ .map(|(_, bp)| bp.kind)
6321+ .unwrap_or(BreakpointKind::Standard),
6322+ );
6323+
60486324 IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
60496325 .shape(ui::IconButtonShape::Square)
60506326 .icon_size(IconSize::XSmall)
6051− .icon_color(Color::Muted)
6327+ .icon_color(color)
60526328 .toggle_state(is_active)
60536329 .on_click(cx.listener(move |editor, _e, window, cx| {
60546330 window.focus(&editor.focus_handle(cx));
@@ -6060,6 +6336,16 @@ impl Editor { · 5 unmodified lines
60606336 cx,
60616337 );
60626338 }))
6339+ .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
6340+ editor.set_breakpoint_context_menu(
6341+ row,
6342+ position,
6343+ bp_kind.clone(),
6344+ event.down.position,
6345+ window,
6346+ cx,
6347+ );
6348+ }))
60636349 }
60646350
60656351 pub fn context_menu_visible(&self) -> bool {
@@ -8032,6 +8318,235 @@ impl Editor { · 1966 unmodified lines
80328318 }
80338319 }
80348320
8321+ fn set_breakpoint_context_menu(
8322+ &mut self,
8323+ row: DisplayRow,
8324+ position: Option<Anchor>,
8325+ kind: Arc<BreakpointKind>,
8326+ clicked_point: gpui::Point<Pixels>,
8327+ window: &mut Window,
8328+ cx: &mut Context<Self>,
8329+ ) {
8330+ if !cx.has_flag::<Debugger>() {
8331+ return;
8332+ }
8333+ let source = self
8334+ .buffer
8335+ .read(cx)
8336+ .snapshot(cx)
8337+ .anchor_before(Point::new(row.0, 0u32));
8338+
8339+ let context_menu =
8340+ self.breakpoint_context_menu(position.unwrap_or(source), kind, window, cx);
8341+
8342+ self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
8343+ self,
8344+ source,
8345+ clicked_point,
8346+ context_menu,
8347+ window,
8348+ cx,
8349+ );
8350+ }
8351+
8352+ fn add_edit_breakpoint_block(
8353+ &mut self,
8354+ anchor: Anchor,
8355+ kind: &BreakpointKind,
8356+ window: &mut Window,
8357+ cx: &mut Context<Self>,
8358+ ) {
8359+ let weak_editor = cx.weak_entity();
8360+ let bp_prompt =
8361+ cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx));
8362+
8363+ let height = bp_prompt.update(cx, |this, cx| {
8364+ this.prompt
8365+ .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2)
8366+ });
8367+ let cloned_prompt = bp_prompt.clone();
8368+ let blocks = vec![BlockProperties {
8369+ style: BlockStyle::Sticky,
8370+ placement: BlockPlacement::Above(anchor),
8371+ height,
8372+ render: Arc::new(move |cx| {
8373+ *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions;
8374+ cloned_prompt.clone().into_any_element()
8375+ }),
8376+ priority: 0,
8377+ }];
8378+
8379+ let focus_handle = bp_prompt.focus_handle(cx);
8380+ window.focus(&focus_handle);
8381+
8382+ let block_ids = self.insert_blocks(blocks, None, cx);
8383+ bp_prompt.update(cx, |prompt, _| {
8384+ prompt.add_block_ids(block_ids);
8385+ });
8386+ }
8387+
8388+ pub(crate) fn breakpoint_at_cursor_head(
8389+ &self,
8390+ window: &mut Window,
8391+ cx: &mut Context<Self>,
8392+ ) -> Option<(Anchor, Breakpoint)> {
8393+ let cursor_position: Point = self.selections.newest(cx).head();
8394+ let snapshot = self.snapshot(window, cx);
8395+ // We Set the column position to zero so this function interacts correctly
8396+ // between calls by clicking on the gutter & using an action to toggle a
8397+ // breakpoint. Otherwise, toggling a breakpoint through an action wouldn't
8398+ // untoggle a breakpoint that was added through clicking on the gutter
8399+ let cursor_position = snapshot
8400+ .display_snapshot
8401+ .buffer_snapshot
8402+ .anchor_before(Point::new(cursor_position.row, 0));
8403+
8404+ let project = self.project.clone();
8405+
8406+ let buffer_id = cursor_position.text_anchor.buffer_id?;
8407+ let enclosing_excerpt = snapshot
8408+ .buffer_snapshot
8409+ .excerpt_ids_for_range(cursor_position..cursor_position)
8410+ .next()?;
8411+ let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
8412+ let buffer_snapshot = buffer.read(cx).snapshot();
8413+
8414+ let row = buffer_snapshot
8415+ .summary_for_anchor::<text::PointUtf16>(&cursor_position.text_anchor)
8416+ .row;
8417+
8418+ let bp = self
8419+ .breakpoint_store
8420+ .as_ref()?
8421+ .read_with(cx, |breakpoint_store, cx| {
8422+ breakpoint_store
8423+ .breakpoints(
8424+ &buffer,
8425+ Some(cursor_position.text_anchor..(text::Anchor::MAX)),
8426+ buffer_snapshot.clone(),
8427+ cx,
8428+ )
8429+ .next()
8430+ .and_then(move |(anchor, bp)| {
8431+ let breakpoint_row = buffer_snapshot
8432+ .summary_for_anchor::<text::PointUtf16>(anchor)
8433+ .row;
8434+
8435+ if breakpoint_row == row {
8436+ snapshot
8437+ .buffer_snapshot
8438+ .anchor_in_excerpt(enclosing_excerpt, *anchor)
8439+ .map(|anchor| (anchor, bp.clone()))
8440+ } else {
8441+ None
8442+ }
8443+ })
8444+ });
8445+ bp
8446+ }
8447+
8448+ pub fn edit_log_breakpoint(
8449+ &mut self,
8450+ _: &EditLogBreakpoint,
8451+ window: &mut Window,
8452+ cx: &mut Context<Self>,
8453+ ) {
8454+ let (anchor, bp) = self
8455+ .breakpoint_at_cursor_head(window, cx)
8456+ .unwrap_or_else(|| {
8457+ let cursor_position: Point = self.selections.newest(cx).head();
8458+
8459+ let breakpoint_position = self
8460+ .snapshot(window, cx)
8461+ .display_snapshot
8462+ .buffer_snapshot
8463+ .anchor_before(Point::new(cursor_position.row, 0));
8464+
8465+ (
8466+ breakpoint_position,
8467+ Breakpoint {
8468+ kind: BreakpointKind::Standard,
8469+ },
8470+ )
8471+ });
8472+
8473+ self.add_edit_breakpoint_block(anchor, &bp.kind, window, cx);
8474+ }
8475+
8476+ pub fn toggle_breakpoint(
8477+ &mut self,
8478+ _: &crate::actions::ToggleBreakpoint,
8479+ window: &mut Window,
8480+ cx: &mut Context<Self>,
8481+ ) {
8482+ let edit_action = BreakpointEditAction::Toggle;
8483+
8484+ if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
8485+ self.edit_breakpoint_at_anchor(anchor, breakpoint.kind, edit_action, cx);
8486+ } else {
8487+ let cursor_position: Point = self.selections.newest(cx).head();
8488+
8489+ let breakpoint_position = self
8490+ .snapshot(window, cx)
8491+ .display_snapshot
8492+ .buffer_snapshot
8493+ .anchor_before(Point::new(cursor_position.row, 0));
8494+
8495+ self.edit_breakpoint_at_anchor(
8496+ breakpoint_position,
8497+ BreakpointKind::Standard,
8498+ edit_action,
8499+ cx,
8500+ );
8501+ }
8502+ }
8503+
8504+ pub fn edit_breakpoint_at_anchor(
8505+ &mut self,
8506+ breakpoint_position: Anchor,
8507+ kind: BreakpointKind,
8508+ edit_action: BreakpointEditAction,
8509+ cx: &mut Context<Self>,
8510+ ) {
8511+ let Some(breakpoint_store) = &self.breakpoint_store else {
8512+ return;
8513+ };
8514+
8515+ let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| {
8516+ if breakpoint_position == Anchor::min() {
8517+ self.buffer()
8518+ .read(cx)
8519+ .excerpt_buffer_ids()
8520+ .into_iter()
8521+ .next()
8522+ } else {
8523+ None
8524+ }
8525+ }) else {
8526+ return;
8527+ };
8528+
8529+ let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
8530+ return;
8531+ };
8532+
8533+ breakpoint_store.update(cx, |breakpoint_store, cx| {
8534+ breakpoint_store.toggle_breakpoint(
8535+ buffer,
8536+ (breakpoint_position.text_anchor, Breakpoint { kind }),
8537+ edit_action,
8538+ cx,
8539+ );
8540+ });
8541+
8542+ cx.notify();
8543+ }
8544+
8545+ #[cfg(any(test, feature = "test-support"))]
8546+ pub fn breakpoint_store(&self) -> Option<Entity<BreakpointStore>> {
8547+ self.breakpoint_store.clone()
8548+ }
8549+
80358550 pub fn prepare_restore_change(
80368551 &self,
80378552 revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
@@ -11866,6 +12381,33 @@ impl Editor { · 3828 unmodified lines
1186612381 .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
1186712382 }
1186812383
12384+ fn go_to_line<T: 'static>(
12385+ &mut self,
12386+ position: Anchor,
12387+ highlight_color: Option<Hsla>,
12388+ window: &mut Window,
12389+ cx: &mut Context<Self>,
12390+ ) {
12391+ let snapshot = self.snapshot(window, cx).display_snapshot;
12392+ let position = position.to_point(&snapshot.buffer_snapshot);
12393+ let start = snapshot
12394+ .buffer_snapshot
12395+ .clip_point(Point::new(position.row, 0), Bias::Left);
12396+ let end = start + Point::new(1, 0);
12397+ let start = snapshot.buffer_snapshot.anchor_before(start);
12398+ let end = snapshot.buffer_snapshot.anchor_before(end);
12399+
12400+ self.clear_row_highlights::<T>();
12401+ self.highlight_rows::<T>(
12402+ start..end,
12403+ highlight_color
12404+ .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
12405+ true,
12406+ cx,
12407+ );
12408+ self.request_autoscroll(Autoscroll::center(), cx);
12409+ }
12410+
1186912411 pub fn go_to_definition(
1187012412 &mut self,
1187112413 _: &GoToDefinition,
@@ -14532,6 +15074,11 @@ impl Editor { · 2660 unmodified lines
1453215074 cx.notify();
1453315075 }
1453415076
15077+ pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context<Self>) {
15078+ self.show_breakpoints = Some(show_breakpoints);
15079+ cx.notify();
15080+ }
15081+
1453515082 pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
1453615083 if self.display_map.read(cx).masked != masked {
1453715084 self.display_map.update(cx, |map, _| map.masked = masked);
@@ -14634,6 +15181,61 @@ impl Editor { · 96 unmodified lines
1463415181 }
1463515182 }
1463615183
15184+ pub fn project_path(&self, cx: &mut Context<Self>) -> Option<ProjectPath> {
15185+ if let Some(buffer) = self.buffer.read(cx).as_singleton() {
15186+ buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))
15187+ } else {
15188+ None
15189+ }
15190+ }
15191+
15192+ pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context<Self>) {
15193+ let _ = maybe!({
15194+ let breakpoint_store = self.breakpoint_store.as_ref()?;
15195+
15196+ let Some((_, _, active_position)) =
15197+ breakpoint_store.read(cx).active_position().cloned()
15198+ else {
15199+ self.clear_row_highlights::<DebugCurrentRowHighlight>();
15200+ return None;
15201+ };
15202+
15203+ let snapshot = self
15204+ .project
15205+ .as_ref()?
15206+ .read(cx)
15207+ .buffer_for_id(active_position.buffer_id?, cx)?
15208+ .read(cx)
15209+ .snapshot();
15210+
15211+ for (id, ExcerptRange { context, .. }) in self
15212+ .buffer
15213+ .read(cx)
15214+ .excerpts_for_buffer(active_position.buffer_id?, cx)
15215+ {
15216+ if context.start.cmp(&active_position, &snapshot).is_ge()
15217+ || context.end.cmp(&active_position, &snapshot).is_lt()
15218+ {
15219+ continue;
15220+ }
15221+ let snapshot = self.buffer.read(cx).snapshot(cx);
15222+ let multibuffer_anchor = snapshot.anchor_in_excerpt(id, active_position)?;
15223+
15224+ self.clear_row_highlights::<DebugCurrentRowHighlight>();
15225+ self.go_to_line::<DebugCurrentRowHighlight>(
15226+ multibuffer_anchor,
15227+ Some(cx.theme().colors().editor_debugger_active_line_background),
15228+ window,
15229+ cx,
15230+ );
15231+
15232+ cx.notify();
15233+ }
15234+
15235+ Some(())
15236+ });
15237+ }
15238+
1463715239 pub fn copy_file_name_without_extension(
1463815240 &mut self,
1463915241 _: &CopyFileNameWithoutExtension,
@@ -17645,6 +18247,7 @@ impl EditorSnapshot { · 3005 unmodified lines
1764518247 .unwrap_or(gutter_settings.code_actions);
1764618248
1764718249 let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables);
18250+ let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints);
1764818251
1764918252 let git_blame_entries_width =
1765018253 self.git_blame_gutter_max_author_length
@@ -17668,7 +18271,7 @@ impl EditorSnapshot { · 17 unmodified lines
1766818271 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
1766918272 left_padding += if !is_singleton {
1767018273 em_width * 4.0
17671− } else if show_code_actions || show_runnables {
18274+ } else if show_code_actions || show_runnables || show_breakpoints {
1767218275 em_width * 3.0
1767318276 } else if show_git_gutter && show_line_numbers {
1767418277 em_width * 2.0
@@ -18668,6 +19271,157 @@ impl Global for KillRing {} · 993 unmodified lines
1866819271
1866919272 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
1867019273
19274+struct BreakpointPromptEditor {
19275+ pub(crate) prompt: Entity<Editor>,
19276+ editor: WeakEntity<Editor>,
19277+ breakpoint_anchor: Anchor,
19278+ kind: BreakpointKind,
19279+ block_ids: HashSet<CustomBlockId>,
19280+ gutter_dimensions: Arc<Mutex<GutterDimensions>>,
19281+ _subscriptions: Vec<Subscription>,
19282+}
19283+
19284+impl BreakpointPromptEditor {
19285+ const MAX_LINES: u8 = 4;
19286+
19287+ fn new(
19288+ editor: WeakEntity<Editor>,
19289+ breakpoint_anchor: Anchor,
19290+ kind: BreakpointKind,
19291+ window: &mut Window,
19292+ cx: &mut Context<Self>,
19293+ ) -> Self {
19294+ let buffer = cx.new(|cx| {
19295+ Buffer::local(
19296+ kind.log_message()
19297+ .map(|msg| msg.to_string())
19298+ .unwrap_or_default(),
19299+ cx,
19300+ )
19301+ });
19302+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19303+
19304+ let prompt = cx.new(|cx| {
19305+ let mut prompt = Editor::new(
19306+ EditorMode::AutoHeight {
19307+ max_lines: Self::MAX_LINES as usize,
19308+ },
19309+ buffer,
19310+ None,
19311+ window,
19312+ cx,
19313+ );
19314+ prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
19315+ prompt.set_show_cursor_when_unfocused(false, cx);
19316+ prompt.set_placeholder_text(
19317+ "Message to log when breakpoint is hit. Expressions within {} are interpolated.",
19318+ cx,
19319+ );
19320+
19321+ prompt
19322+ });
19323+
19324+ Self {
19325+ prompt,
19326+ editor,
19327+ breakpoint_anchor,
19328+ kind,
19329+ gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
19330+ block_ids: Default::default(),
19331+ _subscriptions: vec![],
19332+ }
19333+ }
19334+
19335+ pub(crate) fn add_block_ids(&mut self, block_ids: Vec<CustomBlockId>) {
19336+ self.block_ids.extend(block_ids)
19337+ }
19338+
19339+ fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
19340+ if let Some(editor) = self.editor.upgrade() {
19341+ let log_message = self
19342+ .prompt
19343+ .read(cx)
19344+ .buffer
19345+ .read(cx)
19346+ .as_singleton()
19347+ .expect("A multi buffer in breakpoint prompt isn't possible")
19348+ .read(cx)
19349+ .as_rope()
19350+ .to_string();
19351+
19352+ editor.update(cx, |editor, cx| {
19353+ editor.edit_breakpoint_at_anchor(
19354+ self.breakpoint_anchor,
19355+ self.kind.clone(),
19356+ BreakpointEditAction::EditLogMessage(log_message.into()),
19357+ cx,
19358+ );
19359+
19360+ editor.remove_blocks(self.block_ids.clone(), None, cx);
19361+ cx.focus_self(window);
19362+ });
19363+ }
19364+ }
19365+
19366+ fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
19367+ self.editor
19368+ .update(cx, |editor, cx| {
19369+ editor.remove_blocks(self.block_ids.clone(), None, cx);
19370+ window.focus(&editor.focus_handle);
19371+ })
19372+ .log_err();
19373+ }
19374+
19375+ fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
19376+ let settings = ThemeSettings::get_global(cx);
19377+ let text_style = TextStyle {
19378+ color: if self.prompt.read(cx).read_only(cx) {
19379+ cx.theme().colors().text_disabled
19380+ } else {
19381+ cx.theme().colors().text
19382+ },
19383+ font_family: settings.buffer_font.family.clone(),
19384+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
19385+ font_size: settings.buffer_font_size(cx).into(),
19386+ font_weight: settings.buffer_font.weight,
19387+ line_height: relative(settings.buffer_line_height.value()),
19388+ ..Default::default()
19389+ };
19390+ EditorElement::new(
19391+ &self.prompt,
19392+ EditorStyle {
19393+ background: cx.theme().colors().editor_background,
19394+ local_player: cx.theme().players().local(),
19395+ text: text_style,
19396+ ..Default::default()
19397+ },
19398+ )
19399+ }
19400+}
19401+
19402+impl Render for BreakpointPromptEditor {
19403+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
19404+ let gutter_dimensions = *self.gutter_dimensions.lock();
19405+ h_flex()
19406+ .key_context("Editor")
19407+ .bg(cx.theme().colors().editor_background)
19408+ .border_y_1()
19409+ .border_color(cx.theme().status().info_border)
19410+ .size_full()
19411+ .py(window.line_height() / 2.5)
19412+ .on_action(cx.listener(Self::confirm))
19413+ .on_action(cx.listener(Self::cancel))
19414+ .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)))
19415+ .child(div().flex_1().child(self.render_prompt_editor(cx)))
19416+ }
19417+}
19418+
19419+impl Focusable for BreakpointPromptEditor {
19420+ fn focus_handle(&self, cx: &App) -> FocusHandle {
19421+ self.prompt.focus_handle(cx)
19422+ }
19423+}
19424+
1867119425 fn all_edits_insertions_or_deletions(
1867219426 edits: &Vec<(Range<Anchor>, String)>,
1867319427 snapshot: &MultiBufferSnapshot,
crates/editor/src/editor_settings.rs+5−0
@@ -117,6 +117,7 @@ pub struct Gutter { · 116 unmodified lines
117117 pub line_numbers: bool,
118118 pub code_actions: bool,
119119 pub runnables: bool,
120+ pub breakpoints: bool,
120121 pub folds: bool,
121122 }
122123
@@ -464,6 +465,10 @@ pub struct GutterContent { · 341 unmodified lines
464465 ///
465466 /// Default: true
466467 pub runnables: Option<bool>,
468+ /// Whether to show breakpoints in the gutter.
469+ ///
470+ /// Default: true
471+ pub breakpoints: Option<bool>,
467472 /// Whether to show fold buttons in the gutter.
468473 ///
469474 /// Default: true
crates/editor/src/editor_tests.rs+337−2
@@ -28,8 +28,11 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings}; · 27 unmodified lines
2828 use multi_buffer::{IndentGuide, PathKey};
2929 use parking_lot::Mutex;
3030 use pretty_assertions::{assert_eq, assert_ne};
31−use project::project_settings::{LspSettings, ProjectSettings};
32−use project::FakeFs;
31+use project::{
32+ debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint},
33+ project_settings::{LspSettings, ProjectSettings},
34+ FakeFs,
35+};
3336 use serde_json::{self, json};
3437 use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
3538 use std::{
@@ -11924,6 +11927,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) { · 11888 unmodified lines
1192411927 cx.update_editor(|editor, window, cx| {
1192511928 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
1192611929 });
11930+ cx.run_until_parked();
1192711931 cx.assert_editor_state(after);
1192811932 };
1192911933
@@ -17111,6 +17115,337 @@ async fn assert_highlighted_edits( · 5181 unmodified lines
1711117115 });
1711217116 }
1711317117
17118+#[track_caller]
17119+fn assert_breakpoint(
17120+ breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
17121+ path: &Arc<Path>,
17122+ expected: Vec<(u32, BreakpointKind)>,
17123+) {
17124+ if expected.len() == 0usize {
17125+ assert!(!breakpoints.contains_key(path));
17126+ } else {
17127+ let mut breakpoint = breakpoints
17128+ .get(path)
17129+ .unwrap()
17130+ .into_iter()
17131+ .map(|breakpoint| (breakpoint.position, breakpoint.kind.clone()))
17132+ .collect::<Vec<_>>();
17133+
17134+ breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17135+
17136+ assert_eq!(expected, breakpoint);
17137+ }
17138+}
17139+
17140+fn add_log_breakpoint_at_cursor(
17141+ editor: &mut Editor,
17142+ log_message: &str,
17143+ window: &mut Window,
17144+ cx: &mut Context<Editor>,
17145+) {
17146+ let (anchor, bp) = editor
17147+ .breakpoint_at_cursor_head(window, cx)
17148+ .unwrap_or_else(|| {
17149+ let cursor_position: Point = editor.selections.newest(cx).head();
17150+
17151+ let breakpoint_position = editor
17152+ .snapshot(window, cx)
17153+ .display_snapshot
17154+ .buffer_snapshot
17155+ .anchor_before(Point::new(cursor_position.row, 0));
17156+
17157+ let kind = BreakpointKind::Log(Arc::from(log_message));
17158+
17159+ (breakpoint_position, Breakpoint { kind })
17160+ });
17161+
17162+ editor.edit_breakpoint_at_anchor(
17163+ anchor,
17164+ bp.kind,
17165+ BreakpointEditAction::EditLogMessage(log_message.into()),
17166+ cx,
17167+ );
17168+}
17169+
17170+#[gpui::test]
17171+async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17172+ init_test(cx, |_| {});
17173+
17174+ let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17175+ let fs = FakeFs::new(cx.executor());
17176+ fs.insert_tree(
17177+ path!("/a"),
17178+ json!({
17179+ "main.rs": sample_text,
17180+ }),
17181+ )
17182+ .await;
17183+ let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17184+ let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17185+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17186+
17187+ let fs = FakeFs::new(cx.executor());
17188+ fs.insert_tree(
17189+ path!("/a"),
17190+ json!({
17191+ "main.rs": sample_text,
17192+ }),
17193+ )
17194+ .await;
17195+ let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17196+ let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17197+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17198+ let worktree_id = workspace
17199+ .update(cx, |workspace, _window, cx| {
17200+ workspace.project().update(cx, |project, cx| {
17201+ project.worktrees(cx).next().unwrap().read(cx).id()
17202+ })
17203+ })
17204+ .unwrap();
17205+
17206+ let buffer = project
17207+ .update(cx, |project, cx| {
17208+ project.open_buffer((worktree_id, "main.rs"), cx)
17209+ })
17210+ .await
17211+ .unwrap();
17212+
17213+ let (editor, cx) = cx.add_window_view(|window, cx| {
17214+ Editor::new(
17215+ EditorMode::Full,
17216+ MultiBuffer::build_from_buffer(buffer, cx),
17217+ Some(project.clone()),
17218+ window,
17219+ cx,
17220+ )
17221+ });
17222+
17223+ let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17224+ let abs_path = project.read_with(cx, |project, cx| {
17225+ project
17226+ .absolute_path(&project_path, cx)
17227+ .map(|path_buf| Arc::from(path_buf.to_owned()))
17228+ .unwrap()
17229+ });
17230+
17231+ // assert we can add breakpoint on the first line
17232+ editor.update_in(cx, |editor, window, cx| {
17233+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17234+ editor.move_to_end(&MoveToEnd, window, cx);
17235+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17236+ });
17237+
17238+ let breakpoints = editor.update(cx, |editor, cx| {
17239+ editor
17240+ .breakpoint_store()
17241+ .as_ref()
17242+ .unwrap()
17243+ .read(cx)
17244+ .all_breakpoints(cx)
17245+ .clone()
17246+ });
17247+
17248+ assert_eq!(1, breakpoints.len());
17249+ assert_breakpoint(
17250+ &breakpoints,
17251+ &abs_path,
17252+ vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
17253+ );
17254+
17255+ editor.update_in(cx, |editor, window, cx| {
17256+ editor.move_to_beginning(&MoveToBeginning, window, cx);
17257+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17258+ });
17259+
17260+ let breakpoints = editor.update(cx, |editor, cx| {
17261+ editor
17262+ .breakpoint_store()
17263+ .as_ref()
17264+ .unwrap()
17265+ .read(cx)
17266+ .all_breakpoints(cx)
17267+ .clone()
17268+ });
17269+
17270+ assert_eq!(1, breakpoints.len());
17271+ assert_breakpoint(&breakpoints, &abs_path, vec![(3, BreakpointKind::Standard)]);
17272+
17273+ editor.update_in(cx, |editor, window, cx| {
17274+ editor.move_to_end(&MoveToEnd, window, cx);
17275+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17276+ });
17277+
17278+ let breakpoints = editor.update(cx, |editor, cx| {
17279+ editor
17280+ .breakpoint_store()
17281+ .as_ref()
17282+ .unwrap()
17283+ .read(cx)
17284+ .all_breakpoints(cx)
17285+ .clone()
17286+ });
17287+
17288+ assert_eq!(0, breakpoints.len());
17289+ assert_breakpoint(&breakpoints, &abs_path, vec![]);
17290+}
17291+
17292+#[gpui::test]
17293+async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17294+ init_test(cx, |_| {});
17295+
17296+ let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17297+
17298+ let fs = FakeFs::new(cx.executor());
17299+ fs.insert_tree(
17300+ path!("/a"),
17301+ json!({
17302+ "main.rs": sample_text,
17303+ }),
17304+ )
17305+ .await;
17306+ let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17307+ let (workspace, cx) =
17308+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17309+
17310+ let worktree_id = workspace.update(cx, |workspace, cx| {
17311+ workspace.project().update(cx, |project, cx| {
17312+ project.worktrees(cx).next().unwrap().read(cx).id()
17313+ })
17314+ });
17315+
17316+ let buffer = project
17317+ .update(cx, |project, cx| {
17318+ project.open_buffer((worktree_id, "main.rs"), cx)
17319+ })
17320+ .await
17321+ .unwrap();
17322+
17323+ let (editor, cx) = cx.add_window_view(|window, cx| {
17324+ Editor::new(
17325+ EditorMode::Full,
17326+ MultiBuffer::build_from_buffer(buffer, cx),
17327+ Some(project.clone()),
17328+ window,
17329+ cx,
17330+ )
17331+ });
17332+
17333+ let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17334+ let abs_path = project.read_with(cx, |project, cx| {
17335+ project
17336+ .absolute_path(&project_path, cx)
17337+ .map(|path_buf| Arc::from(path_buf.to_owned()))
17338+ .unwrap()
17339+ });
17340+
17341+ editor.update_in(cx, |editor, window, cx| {
17342+ add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17343+ });
17344+
17345+ let breakpoints = editor.update(cx, |editor, cx| {
17346+ editor
17347+ .breakpoint_store()
17348+ .as_ref()
17349+ .unwrap()
17350+ .read(cx)
17351+ .all_breakpoints(cx)
17352+ .clone()
17353+ });
17354+
17355+ assert_breakpoint(
17356+ &breakpoints,
17357+ &abs_path,
17358+ vec![(0, BreakpointKind::Log("hello world".into()))],
17359+ );
17360+
17361+ // Removing a log message from a log breakpoint should remove it
17362+ editor.update_in(cx, |editor, window, cx| {
17363+ add_log_breakpoint_at_cursor(editor, "", window, cx);
17364+ });
17365+
17366+ let breakpoints = editor.update(cx, |editor, cx| {
17367+ editor
17368+ .breakpoint_store()
17369+ .as_ref()
17370+ .unwrap()
17371+ .read(cx)
17372+ .all_breakpoints(cx)
17373+ .clone()
17374+ });
17375+
17376+ assert_breakpoint(&breakpoints, &abs_path, vec![]);
17377+
17378+ editor.update_in(cx, |editor, window, cx| {
17379+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17380+ editor.move_to_end(&MoveToEnd, window, cx);
17381+ editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17382+ // Not adding a log message to a standard breakpoint shouldn't remove it
17383+ add_log_breakpoint_at_cursor(editor, "", window, cx);
17384+ });
17385+
17386+ let breakpoints = editor.update(cx, |editor, cx| {
17387+ editor
17388+ .breakpoint_store()
17389+ .as_ref()
17390+ .unwrap()
17391+ .read(cx)
17392+ .all_breakpoints(cx)
17393+ .clone()
17394+ });
17395+
17396+ assert_breakpoint(
17397+ &breakpoints,
17398+ &abs_path,
17399+ vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
17400+ );
17401+
17402+ editor.update_in(cx, |editor, window, cx| {
17403+ add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17404+ });
17405+
17406+ let breakpoints = editor.update(cx, |editor, cx| {
17407+ editor
17408+ .breakpoint_store()
17409+ .as_ref()
17410+ .unwrap()
17411+ .read(cx)
17412+ .all_breakpoints(cx)
17413+ .clone()
17414+ });
17415+
17416+ assert_breakpoint(
17417+ &breakpoints,
17418+ &abs_path,
17419+ vec![
17420+ (0, BreakpointKind::Standard),
17421+ (3, BreakpointKind::Log("hello world".into())),
17422+ ],
17423+ );
17424+
17425+ editor.update_in(cx, |editor, window, cx| {
17426+ add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17427+ });
17428+
17429+ let breakpoints = editor.update(cx, |editor, cx| {
17430+ editor
17431+ .breakpoint_store()
17432+ .as_ref()
17433+ .unwrap()
17434+ .read(cx)
17435+ .all_breakpoints(cx)
17436+ .clone()
17437+ });
17438+
17439+ assert_breakpoint(
17440+ &breakpoints,
17441+ &abs_path,
17442+ vec![
17443+ (0, BreakpointKind::Standard),
17444+ (3, BreakpointKind::Log("hello Earth !!".into())),
17445+ ],
17446+ );
17447+}
17448+
1711417449 #[gpui::test]
1711517450 async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
1711617451 init_test(cx, |_| {});
crates/editor/src/element.rs+199−41
@@ -29,6 +29,7 @@ use crate::{ · 28 unmodified lines
2929 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
3030 use client::ParticipantIndex;
3131 use collections::{BTreeMap, HashMap, HashSet};
32+use feature_flags::{Debugger, FeatureFlagAppExt};
3233 use file_icons::FileIcons;
3334 use git::{blame::BlameEntry, status::FileStatus, Oid};
3435 use gpui::{
@@ -55,7 +56,10 @@ use multi_buffer::{ · 20 unmodified lines
5556 Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,
5657 MultiBufferRow, RowInfo,
5758 };
58−use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
59+use project::{
60+ debugger::breakpoint_store::{Breakpoint, BreakpointKind},
61+ project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
62+};
5963 use settings::Settings;
6064 use smallvec::{smallvec, SmallVec};
6165 use std::{
@@ -82,6 +86,14 @@ use workspace::{item::Item, notifications::NotifyTaskExt}; · 20 unmodified lines
8286 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
8387 const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
8488
89+/// Determines what kinds of highlights should be applied to a lines background.
90+#[derive(Clone, Copy, Default)]
91+struct LineHighlightSpec {
92+ selection: bool,
93+ breakpoint: bool,
94+ _active_stack_frame: bool,
95+}
96+
8597 struct SelectionLayout {
8698 head: DisplayPoint,
8799 cursor_shape: CursorShape,
@@ -509,6 +521,10 @@ impl EditorElement { · 421 unmodified lines
509521 register_action(editor, window, Editor::insert_uuid_v4);
510522 register_action(editor, window, Editor::insert_uuid_v7);
511523 register_action(editor, window, Editor::open_selections_in_multibuffer);
524+ if cx.has_flag::<Debugger>() {
525+ register_action(editor, window, Editor::toggle_breakpoint);
526+ register_action(editor, window, Editor::edit_log_breakpoint);
527+ }
512528 }
513529
514530 fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
@@ -875,6 +891,18 @@ impl EditorElement { · 360 unmodified lines
875891 let gutter_hovered = gutter_hitbox.is_hovered(window);
876892 editor.set_gutter_hovered(gutter_hovered, cx);
877893
894+ if gutter_hovered {
895+ editor.gutter_breakpoint_indicator = Some(
896+ position_map
897+ .point_for_position(event.position)
898+ .previous_valid,
899+ );
900+ } else {
901+ editor.gutter_breakpoint_indicator = None;
902+ }
903+
904+ cx.notify();
905+
878906 // Don't trigger hover popover if mouse is hovering over context menu
879907 if text_hitbox.is_hovered(window) {
880908 let point_for_position = position_map.point_for_position(event.position);
@@ -970,7 +998,7 @@ impl EditorElement { · 89 unmodified lines
970998 cx: &mut App,
971999 ) -> (
9721000 Vec<(PlayerColor, Vec<SelectionLayout>)>,
973− BTreeMap<DisplayRow, bool>,
1001+ BTreeMap<DisplayRow, LineHighlightSpec>,
9741002 Option<DisplayPoint>,
9751003 ) {
9761004 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
@@ -1000,9 +1028,10 @@ impl EditorElement { · 23 unmodified lines
10001028 for row in cmp::max(layout.active_rows.start.0, start_row.0)
10011029 ..=cmp::min(layout.active_rows.end.0, end_row.0)
10021030 {
1003− let contains_non_empty_selection =
1004− active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
1005− *contains_non_empty_selection |= !is_empty;
1031+ let contains_non_empty_selection = active_rows
1032+ .entry(DisplayRow(row))
1033+ .or_insert_with(LineHighlightSpec::default);
1034+ contains_non_empty_selection.selection |= !is_empty;
10061035 }
10071036 layouts.push(layout);
10081037 }
@@ -2019,6 +2048,54 @@ impl EditorElement { · 1010 unmodified lines
20192048 (offset_y, length)
20202049 }
20212050
2051+ #[allow(clippy::too_many_arguments)]
2052+ fn layout_breakpoints(
2053+ &self,
2054+ line_height: Pixels,
2055+ range: Range<DisplayRow>,
2056+ scroll_pixel_position: gpui::Point<Pixels>,
2057+ gutter_dimensions: &GutterDimensions,
2058+ gutter_hitbox: &Hitbox,
2059+ display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2060+ snapshot: &EditorSnapshot,
2061+ breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint)>,
2062+ window: &mut Window,
2063+ cx: &mut App,
2064+ ) -> Vec<AnyElement> {
2065+ self.editor.update(cx, |editor, cx| {
2066+ breakpoints
2067+ .into_iter()
2068+ .filter_map(|(point, (text_anchor, bp))| {
2069+ let row = MultiBufferRow { 0: point.0 };
2070+
2071+ if range.start > point || range.end < point {
2072+ return None;
2073+ }
2074+
2075+ if snapshot.is_line_folded(row) {
2076+ return None;
2077+ }
2078+
2079+ let button = editor.render_breakpoint(text_anchor, point, &bp.kind, cx);
2080+
2081+ let button = prepaint_gutter_button(
2082+ button,
2083+ point,
2084+ line_height,
2085+ gutter_dimensions,
2086+ scroll_pixel_position,
2087+ gutter_hitbox,
2088+ display_hunks,
2089+ window,
2090+ cx,
2091+ );
2092+ Some(button)
2093+ })
2094+ .collect_vec()
2095+ })
2096+ }
2097+
2098+ #[allow(clippy::too_many_arguments)]
20222099 fn layout_run_indicators(
20232100 &self,
20242101 line_height: Pixels,
@@ -2029,6 +2106,7 @@ impl EditorElement { · 4 unmodified lines
20292106 gutter_hitbox: &Hitbox,
20302107 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
20312108 snapshot: &EditorSnapshot,
2109+ breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
20322110 window: &mut Window,
20332111 cx: &mut App,
20342112 ) -> Vec<AnyElement> {
@@ -2088,6 +2166,7 @@ impl EditorElement { · 53 unmodified lines
20882166 return None;
20892167 }
20902168 }
2169+
20912170 let display_row = multibuffer_point.to_display_point(snapshot).row();
20922171 if row_infos
20932172 .get((display_row - range.start).0 as usize)
@@ -2099,6 +2178,7 @@ impl EditorElement { · 5 unmodified lines
20992178 &self.style,
21002179 Some(display_row) == active_task_indicator_row,
21012180 display_row,
2181+ breakpoints.remove(&display_row),
21022182 cx,
21032183 );
21042184
@@ -2211,6 +2291,7 @@ impl EditorElement { · 106 unmodified lines
22112291 scroll_pixel_position: gpui::Point<Pixels>,
22122292 gutter_dimensions: &GutterDimensions,
22132293 gutter_hitbox: &Hitbox,
2294+ breakpoint_points: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
22142295 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
22152296 window: &mut Window,
22162297 cx: &mut App,
@@ -2226,11 +2307,16 @@ impl EditorElement { · 9 unmodified lines
22262307 {
22272308 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
22282309 };
2229− button = editor.render_code_actions_indicator(&self.style, row, active, cx);
2310+
2311+ let breakpoint = breakpoint_points.get(&row);
2312+ button = editor.render_code_actions_indicator(&self.style, row, active, breakpoint, cx);
22302313 });
22312314
2315+ let button = button?;
2316+ breakpoint_points.remove(&row);
2317+
22322318 let button = prepaint_gutter_button(
2233− button?,
2319+ button,
22342320 row,
22352321 line_height,
22362322 gutter_dimensions,
@@ -2310,6 +2396,7 @@ impl EditorElement { · 73 unmodified lines
23102396 scroll_position: gpui::Point<f32>,
23112397 rows: Range<DisplayRow>,
23122398 buffer_rows: &[RowInfo],
2399+ active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
23132400 newest_selection_head: Option<DisplayPoint>,
23142401 snapshot: &EditorSnapshot,
23152402 window: &mut Window,
@@ -2365,7 +2452,18 @@ impl EditorElement { · 49 unmodified lines
23652452 return None;
23662453 }
23672454
2368− let color = cx.theme().colors().editor_line_number;
2455+ let color = active_rows
2456+ .get(&display_row)
2457+ .and_then(|spec| {
2458+ if spec.breakpoint {
2459+ Some(cx.theme().colors().debugger_accent)
2460+ } else if spec.selection {
2461+ Some(cx.theme().colors().editor_active_line_number)
2462+ } else {
2463+ None
2464+ }
2465+ })
2466+ .unwrap_or_else(|| cx.theme().colors().editor_line_number);
23692467 let shaped_line = self
23702468 .shape_line_number(SharedString::from(&line_number), color, window)
23712469 .log_err()?;
@@ -2396,7 +2494,6 @@ impl EditorElement { · 24 unmodified lines
23962494 let line_number = LineNumberLayout {
23972495 shaped_line,
23982496 hitbox,
2399− display_row,
24002497 };
24012498 Some((multi_buffer_row, line_number))
24022499 })
@@ -2408,7 +2505,7 @@ impl EditorElement { · 5 unmodified lines
24082505 &self,
24092506 rows: Range<DisplayRow>,
24102507 row_infos: &[RowInfo],
2411− active_rows: &BTreeMap<DisplayRow, bool>,
2508+ active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
24122509 snapshot: &EditorSnapshot,
24132510 window: &mut Window,
24142511 cx: &mut App,
@@ -4035,14 +4132,14 @@ impl EditorElement { · 1620 unmodified lines
40354132 .peek()
40364133 .map_or(false, |(active_row, has_selection)| {
40374134 active_row.0 == end_row + 1
4038− && *has_selection == contains_non_empty_selection
4135+ && has_selection.selection == contains_non_empty_selection.selection
40394136 })
40404137 {
40414138 active_rows.next().unwrap();
40424139 end_row += 1;
40434140 }
40444141
4045− if !contains_non_empty_selection {
4142+ if !contains_non_empty_selection.selection {
40464143 let highlight_h_range =
40474144 match layout.position_map.snapshot.current_line_highlight {
40484145 CurrentLineHighlight::Gutter => Some(Range {
@@ -4285,32 +4382,31 @@ impl EditorElement { · 236 unmodified lines
42854382 for LineNumberLayout {
42864383 shaped_line,
42874384 hitbox,
4288− display_row,
42894385 } in layout.line_numbers.values()
42904386 {
42914387 let Some(hitbox) = hitbox else {
42924388 continue;
42934389 };
42944390
4295− let is_active = layout.active_rows.contains_key(&display_row);
4391+ let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
4392+ let color = cx.theme().colors().editor_hover_line_number;
42964393
4297− let color = if is_active {
4298− cx.theme().colors().editor_active_line_number
4299− } else if !is_singleton && hitbox.is_hovered(window) {
4300− cx.theme().colors().editor_hover_line_number
4301− } else {
4302− cx.theme().colors().editor_line_number
4303− };
4394+ let Some(line) = self
4395+ .shape_line_number(shaped_line.text.clone(), color, window)
4396+ .log_err()
4397+ else {
4398+ continue;
4399+ };
43044400
4305− let Some(line) = self
4306− .shape_line_number(shaped_line.text.clone(), color, window)
4307− .log_err()
4308− else {
4309− continue;
4310− };
4311− let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else {
4401+ line.paint(hitbox.origin, line_height, window, cx).log_err()
4402+ } else {
4403+ shaped_line
4404+ .paint(hitbox.origin, line_height, window, cx)
4405+ .log_err()
4406+ }) else {
43124407 continue;
43134408 };
4409+
43144410 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
43154411 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
43164412 if is_singleton {
@@ -4335,7 +4431,7 @@ impl EditorElement { · 18 unmodified lines
43354431 &layout.position_map.snapshot,
43364432 line_height,
43374433 layout.gutter_hitbox.bounds,
4338− hunk,
4434+ &hunk,
43394435 );
43404436 Some((
43414437 hunk_bounds,
@@ -4507,6 +4603,10 @@ impl EditorElement { · 165 unmodified lines
45074603 }
45084604 });
45094605
4606+ for breakpoint in layout.breakpoints.iter_mut() {
4607+ breakpoint.paint(window, cx);
4608+ }
4609+
45104610 for test_indicator in layout.test_indicators.iter_mut() {
45114611 test_indicator.paint(window, cx);
45124612 }
@@ -5707,6 +5807,7 @@ fn prepaint_gutter_button( · 1194 unmodified lines
57075807 cx: &mut App,
57085808 ) -> AnyElement {
57095809 let mut button = button.into_any_element();
5810+
57105811 let available_space = size(
57115812 AvailableSpace::MinContent,
57125813 AvailableSpace::Definite(line_height),
@@ -6871,16 +6972,25 @@ impl Element for EditorElement { · 1158 unmodified lines
68716972 (selections, selected_buffer_ids)
68726973 });
68736974
6874− let (selections, active_rows, newest_selection_head) = self.layout_selections(
6875− start_anchor,
6876− end_anchor,
6877− &local_selections,
6878− &snapshot,
6879− start_row,
6880− end_row,
6881− window,
6882− cx,
6883− );
6975+ let (selections, mut active_rows, newest_selection_head) = self
6976+ .layout_selections(
6977+ start_anchor,
6978+ end_anchor,
6979+ &local_selections,
6980+ &snapshot,
6981+ start_row,
6982+ end_row,
6983+ window,
6984+ cx,
6985+ );
6986+ let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
6987+ editor.active_breakpoints(start_row..end_row, window, cx)
6988+ });
6989+ if cx.has_flag::<Debugger>() {
6990+ for display_row in breakpoint_rows.keys() {
6991+ active_rows.entry(*display_row).or_default().breakpoint = true;
6992+ }
6993+ }
68846994
68856995 let line_numbers = self.layout_line_numbers(
68866996 Some(&gutter_hitbox),
@@ -6889,12 +6999,36 @@ impl Element for EditorElement { · 2 unmodified lines
68896999 scroll_position,
68907000 start_row..end_row,
68917001 &row_infos,
7002+ &active_rows,
68927003 newest_selection_head,
68937004 &snapshot,
68947005 window,
68957006 cx,
68967007 );
68977008
7009+ // We add the gutter breakpoint indicator to breakpoint_rows after painting
7010+ // line numbers so we don't paint a line number debug accent color if a user
7011+ // has their mouse over that line when a breakpoint isn't there
7012+ if cx.has_flag::<Debugger>() {
7013+ let gutter_breakpoint_indicator =
7014+ self.editor.read(cx).gutter_breakpoint_indicator;
7015+ if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator {
7016+ breakpoint_rows
7017+ .entry(gutter_breakpoint_point.row())
7018+ .or_insert_with(|| {
7019+ let position = snapshot.display_point_to_anchor(
7020+ gutter_breakpoint_point,
7021+ Bias::Left,
7022+ );
7023+ let breakpoint = Breakpoint {
7024+ kind: BreakpointKind::Standard,
7025+ };
7026+
7027+ (position, breakpoint)
7028+ });
7029+ }
7030+ }
7031+
68987032 let mut expand_toggles =
68997033 window.with_element_namespace("expand_toggles", |window| {
69007034 self.layout_expand_toggles(
@@ -7339,6 +7473,7 @@ impl Element for EditorElement { · 438 unmodified lines
73397473 scroll_pixel_position,
73407474 &gutter_dimensions,
73417475 &gutter_hitbox,
7476+ &mut breakpoint_rows,
73427477 &display_hunks,
73437478 window,
73447479 cx,
@@ -7370,6 +7505,7 @@ impl Element for EditorElement { · 25 unmodified lines
73707505 &gutter_hitbox,
73717506 &display_hunks,
73727507 &snapshot,
7508+ &mut breakpoint_rows,
73737509 window,
73747510 cx,
73757511 )
@@ -7377,6 +7513,26 @@ impl Element for EditorElement { · 1 unmodified line
73777513 Vec::new()
73787514 };
73797515
7516+ let show_breakpoints = snapshot
7517+ .show_breakpoints
7518+ .unwrap_or(gutter_settings.breakpoints);
7519+ let breakpoints = if cx.has_flag::<Debugger>() && show_breakpoints {
7520+ self.layout_breakpoints(
7521+ line_height,
7522+ start_row..end_row,
7523+ scroll_pixel_position,
7524+ &gutter_dimensions,
7525+ &gutter_hitbox,
7526+ &display_hunks,
7527+ &snapshot,
7528+ breakpoint_rows,
7529+ window,
7530+ cx,
7531+ )
7532+ } else {
7533+ vec![]
7534+ };
7535+
73807536 self.layout_signature_help(
73817537 &hitbox,
73827538 content_origin,
@@ -7526,6 +7682,7 @@ impl Element for EditorElement { · 143 unmodified lines
75267682 diff_hunk_controls,
75277683 mouse_context_menu,
75287684 test_indicators,
7685+ breakpoints,
75297686 code_actions_indicator,
75307687 crease_toggles,
75317688 crease_trailers,
@@ -7688,7 +7845,7 @@ pub struct EditorLayout { · 156 unmodified lines
76887845 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
76897846 indent_guides: Option<Vec<IndentGuideLayout>>,
76907847 visible_display_row_range: Range<DisplayRow>,
7691− active_rows: BTreeMap<DisplayRow, bool>,
7848+ active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
76927849 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
76937850 line_elements: SmallVec<[AnyElement; 1]>,
76947851 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
@@ -7705,6 +7862,7 @@ pub struct EditorLayout { · 10 unmodified lines
77057862 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
77067863 code_actions_indicator: Option<AnyElement>,
77077864 test_indicators: Vec<AnyElement>,
7865+ breakpoints: Vec<AnyElement>,
77087866 crease_toggles: Vec<Option<AnyElement>>,
77097867 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
77107868 diff_hunk_controls: Vec<AnyElement>,
@@ -7725,7 +7883,6 @@ impl EditorLayout { · 14 unmodified lines
77257883 struct LineNumberLayout {
77267884 shaped_line: ShapedLine,
77277885 hitbox: Option<Hitbox>,
7728− display_row: DisplayRow,
77297886 }
77307887
77317888 struct ColoredRange<T> {
@@ -8376,6 +8533,7 @@ mod tests { · 644 unmodified lines
83768533 ..Default::default()
83778534 })
83788535 .collect::<Vec<_>>(),
8536+ &BTreeMap::default(),
83798537 Some(DisplayPoint::new(DisplayRow(0), 0)),
83808538 &snapshot,
83818539 window,
crates/extension/src/extension_host_proxy.rs+3−3
@@ -4,7 +4,7 @@ use std::sync::Arc; · 3 unmodified lines
44 use anyhow::Result;
55 use fs::Fs;
66 use gpui::{App, Global, ReadGlobal, SharedString, Task};
7−use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage};
7+use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
88 use lsp::LanguageServerName;
99 use parking_lot::RwLock;
1010
@@ -284,7 +284,7 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { · 273 unmodified lines
284284 fn update_language_server_status(
285285 &self,
286286 language_server_id: LanguageServerName,
287− status: LanguageServerBinaryStatus,
287+ status: BinaryStatus,
288288 );
289289 }
290290
@@ -317,7 +317,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy { · 26 unmodified lines
317317 fn update_language_server_status(
318318 &self,
319319 language_server_id: LanguageServerName,
320− status: LanguageServerBinaryStatus,
320+ status: BinaryStatus,
321321 ) {
322322 let Some(proxy) = self.language_server_proxy.read().clone() else {
323323 return;
crates/extension_host/src/extension_store_test.rs+5−14
@@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines
88 use extension::ExtensionHostProxy;
99 use fs::{FakeFs, Fs, RealFs};
1010 use futures::{io::BufReader, AsyncReadExt, StreamExt};
11−use gpui::{AppContext as _, SemanticVersion, TestAppContext};
11+use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};
1212 use http_client::{FakeHttpClient, Response};
13−use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
13+use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
1414 use lsp::LanguageServerName;
1515 use node_runtime::NodeRuntime;
1616 use parking_lot::Mutex;
@@ -663,18 +663,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 646 unmodified lines
663663 status_updates.next().await.unwrap(),
664664 ],
665665 [
666− (
667− LanguageServerName("gleam".into()),
668− LanguageServerBinaryStatus::CheckingForUpdate
669− ),
670− (
671− LanguageServerName("gleam".into()),
672− LanguageServerBinaryStatus::Downloading
673− ),
674− (
675− LanguageServerName("gleam".into()),
676− LanguageServerBinaryStatus::None
677− )
666+ (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate),
667+ (SharedString::new("gleam"), BinaryStatus::Downloading),
668+ (SharedString::new("gleam"), BinaryStatus::None)
678669 ]
679670 );
680671
crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs+5−11
@@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4; · 2 unmodified lines
33 use crate::wasm_host::WasmState;
44 use anyhow::Result;
55 use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};
6−use language::LanguageServerBinaryStatus;
6+use language::BinaryStatus;
77 use semantic_version::SemanticVersion;
88 use std::sync::{Arc, OnceLock};
99 use wasmtime::component::{Linker, Resource};
@@ -132,17 +132,11 @@ impl ExtensionImports for WasmState { · 122 unmodified lines
132132 status: LanguageServerInstallationStatus,
133133 ) -> wasmtime::Result<()> {
134134 let status = match status {
135− LanguageServerInstallationStatus::CheckingForUpdate => {
136− LanguageServerBinaryStatus::CheckingForUpdate
137− }
138− LanguageServerInstallationStatus::Downloading => {
139− LanguageServerBinaryStatus::Downloading
140− }
135+ LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
136+ LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
141137 LanguageServerInstallationStatus::Cached
142− | LanguageServerInstallationStatus::Downloaded => LanguageServerBinaryStatus::None,
143− LanguageServerInstallationStatus::Failed(error) => {
144− LanguageServerBinaryStatus::Failed { error }
145− }
138+ | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None,
139+ LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
146140 };
147141
148142 self.host
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs+5−11
@@ -8,7 +8,7 @@ use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDel · 7 unmodified lines
88 use futures::{io::BufReader, FutureExt as _};
99 use futures::{lock::Mutex, AsyncReadExt};
1010 use language::LanguageName;
11−use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus};
11+use language::{language_settings::AllLanguageSettings, BinaryStatus};
1212 use project::project_settings::ProjectSettings;
1313 use semantic_version::SemanticVersion;
1414 use std::{
@@ -474,16 +474,10 @@ impl ExtensionImports for WasmState { · 459 unmodified lines
474474 status: LanguageServerInstallationStatus,
475475 ) -> wasmtime::Result<()> {
476476 let status = match status {
477− LanguageServerInstallationStatus::CheckingForUpdate => {
478− LanguageServerBinaryStatus::CheckingForUpdate
479− }
480− LanguageServerInstallationStatus::Downloading => {
481− LanguageServerBinaryStatus::Downloading
482− }
483− LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
484− LanguageServerInstallationStatus::Failed(error) => {
485− LanguageServerBinaryStatus::Failed { error }
486− }
477+ LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
478+ LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
479+ LanguageServerInstallationStatus::None => BinaryStatus::None,
480+ LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
487481 };
488482
489483 self.host
crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs+5−11
@@ -13,7 +13,7 @@ use extension::{ · 12 unmodified lines
1313 };
1414 use futures::{io::BufReader, FutureExt as _};
1515 use futures::{lock::Mutex, AsyncReadExt};
16−use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
16+use language::{language_settings::AllLanguageSettings, BinaryStatus, LanguageName};
1717 use project::project_settings::ProjectSettings;
1818 use semantic_version::SemanticVersion;
1919 use std::{
@@ -692,16 +692,10 @@ impl ExtensionImports for WasmState { · 672 unmodified lines
692692 status: LanguageServerInstallationStatus,
693693 ) -> wasmtime::Result<()> {
694694 let status = match status {
695− LanguageServerInstallationStatus::CheckingForUpdate => {
696− LanguageServerBinaryStatus::CheckingForUpdate
697− }
698− LanguageServerInstallationStatus::Downloading => {
699− LanguageServerBinaryStatus::Downloading
700− }
701− LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
702− LanguageServerInstallationStatus::Failed(error) => {
703− LanguageServerBinaryStatus::Failed { error }
704− }
695+ LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
696+ LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
697+ LanguageServerInstallationStatus::None => BinaryStatus::None,
698+ LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
705699 };
706700
707701 self.host
crates/feature_flags/src/feature_flags.rs+6−1
@@ -115,6 +115,11 @@ impl FeatureFlag for AutoCommand { · 114 unmodified lines
115115 }
116116 }
117117
118+pub struct Debugger {}
119+impl FeatureFlag for Debugger {
120+ const NAME: &'static str = "debugger";
121+}
122+
118123 pub trait FeatureFlagViewExt<V: 'static> {
119124 fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
120125 where
@@ -202,7 +207,7 @@ impl FeatureFlagAppExt for App { · 81 unmodified lines
202207 fn has_flag<T: FeatureFlag>(&self) -> bool {
203208 self.try_global::<FeatureFlags>()
204209 .map(|flags| flags.has_flag::<T>())
205− .unwrap_or(false)
210+ .unwrap_or_else(|| T::enabled_in_development())
206211 }
207212
208213 fn is_staff(&self) -> bool {
crates/language/src/buffer_tests.rs+1−1
@@ -2539,7 +2539,7 @@ fn test_branch_and_merge(cx: &mut TestAppContext) { · 2538 unmodified lines
25392539 assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");
25402540 });
25412541
2542− // Convert from branch buffer ranges to the corresoponing ranges in the
2542+ // Convert from branch buffer ranges to the corresponding ranges in the
25432543 // base buffer.
25442544 branch.read_with(cx, |buffer, cx| {
25452545 assert_eq!(
crates/language/src/language.rs+8−8
@@ -73,8 +73,8 @@ pub use buffer::Operation; · 72 unmodified lines
7373 pub use buffer::*;
7474 pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
7575 pub use language_registry::{
76− AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry,
77− LanguageServerBinaryStatus, QUERY_FILENAME_PREFIXES,
76+ AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
77+ QUERY_FILENAME_PREFIXES,
7878 };
7979 pub use lsp::{LanguageServerId, LanguageServerName};
8080 pub use outline::*;
@@ -304,7 +304,7 @@ pub trait LspAdapterDelegate: Send + Sync { · 223 unmodified lines
304304 fn worktree_id(&self) -> WorktreeId;
305305 fn worktree_root_path(&self) -> &Path;
306306 fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
307− fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
307+ fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
308308 async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
309309
310310 async fn npm_package_installed_version(
@@ -382,7 +382,7 @@ pub trait LspAdapter: 'static + Send + Sync { · 71 unmodified lines
382382 } else {
383383 delegate.update_status(
384384 self.name(),
385− LanguageServerBinaryStatus::Failed {
385+ BinaryStatus::Failed {
386386 error: format!("{error:?}"),
387387 },
388388 );
@@ -586,7 +586,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> · 197 unmodified lines
586586
587587 let name = adapter.name();
588588 log::info!("fetching latest version of language server {:?}", name.0);
589− delegate.update_status(name.clone(), LanguageServerBinaryStatus::CheckingForUpdate);
589+ delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate);
590590
591591 let latest_version = adapter
592592 .fetch_latest_server_version(delegate.as_ref())
@@ -597,16 +597,16 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> · 4 unmodified lines
597597 .await
598598 {
599599 log::info!("language server {:?} is already installed", name.0);
600− delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
600+ delegate.update_status(name.clone(), BinaryStatus::None);
601601 Ok(binary)
602602 } else {
603603 log::info!("downloading language server {:?}", name.0);
604− delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
604+ delegate.update_status(adapter.name(), BinaryStatus::Downloading);
605605 let binary = adapter
606606 .fetch_server_binary(latest_version, container_dir, delegate.as_ref())
607607 .await;
608608
609− delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
609+ delegate.update_status(name.clone(), BinaryStatus::None);
610610 binary
611611 }
612612 }
crates/language/src/language_registry.rs+22−16
@@ -98,7 +98,8 @@ pub struct LanguageRegistry { · 97 unmodified lines
9898 state: RwLock<LanguageRegistryState>,
9999 language_server_download_dir: Option<Arc<Path>>,
100100 executor: BackgroundExecutor,
101− lsp_binary_status_tx: LspBinaryStatusSender,
101+ lsp_binary_status_tx: BinaryStatusSender,
102+ dap_binary_status_tx: BinaryStatusSender,
102103 }
103104
104105 struct LanguageRegistryState {
@@ -130,7 +131,7 @@ pub struct FakeLanguageServerEntry { · 25 unmodified lines
130131 }
131132
132133 #[derive(Clone, Debug, PartialEq, Eq)]
133−pub enum LanguageServerBinaryStatus {
134+pub enum BinaryStatus {
134135 None,
135136 CheckingForUpdate,
136137 Downloading,
@@ -213,8 +214,8 @@ pub struct LanguageQueries { · 76 unmodified lines
213214 }
214215
215216 #[derive(Clone, Default)]
216−struct LspBinaryStatusSender {
217− txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
217+struct BinaryStatusSender {
218+ txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(SharedString, BinaryStatus)>>>>,
218219 }
219220
220221 pub struct LoadedLanguage {
@@ -247,6 +248,7 @@ impl LanguageRegistry { · 26 unmodified lines
247248 }),
248249 language_server_download_dir: None,
249250 lsp_binary_status_tx: Default::default(),
251+ dap_binary_status_tx: Default::default(),
250252 executor,
251253 };
252254 this.add(PLAIN_TEXT.clone());
@@ -914,12 +916,12 @@ impl LanguageRegistry { · 661 unmodified lines
914916 self.state.read().all_lsp_adapters.get(name).cloned()
915917 }
916918
917− pub fn update_lsp_status(
918− &self,
919− server_name: LanguageServerName,
920− status: LanguageServerBinaryStatus,
921− ) {
922− self.lsp_binary_status_tx.send(server_name, status);
919+ pub fn update_lsp_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
920+ self.lsp_binary_status_tx.send(server_name.0, status);
921+ }
922+
923+ pub fn update_dap_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
924+ self.dap_binary_status_tx.send(server_name.0, status);
923925 }
924926
925927 pub fn next_language_server_id(&self) -> LanguageServerId {
@@ -974,10 +976,16 @@ impl LanguageRegistry { · 48 unmodified lines
974976
975977 pub fn language_server_binary_statuses(
976978 &self,
977− ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
979+ ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
978980 self.lsp_binary_status_tx.subscribe()
979981 }
980982
983+ pub fn dap_server_binary_statuses(
984+ &self,
985+ ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
986+ self.dap_binary_status_tx.subscribe()
987+ }
988+
981989 pub async fn delete_server_container(&self, name: LanguageServerName) {
982990 log::info!("deleting server container");
983991 let Some(dir) = self.language_server_download_dir(&name) else {
@@ -1088,16 +1096,14 @@ impl LanguageRegistryState { · 104 unmodified lines
10881096 }
10891097 }
10901098
1091−impl LspBinaryStatusSender {
1092− fn subscribe(
1093− &self,
1094− ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
1099+impl BinaryStatusSender {
1100+ fn subscribe(&self) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
10951101 let (tx, rx) = mpsc::unbounded();
10961102 self.txs.lock().push(tx);
10971103 rx
10981104 }
10991105
1100− fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) {
1106+ fn send(&self, name: SharedString, status: BinaryStatus) {
11011107 let mut txs = self.txs.lock();
11021108 txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
11031109 }
crates/language_extension/src/extension_lsp_adapter.rs+3−3
@@ -12,8 +12,8 @@ use fs::Fs; · 11 unmodified lines
1212 use futures::{Future, FutureExt};
1313 use gpui::AsyncApp;
1414 use language::{
15− CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus,
16− LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
15+ BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
16+ LspAdapter, LspAdapterDelegate,
1717 };
1818 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
1919 use serde::Serialize;
@@ -80,7 +80,7 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { · 60 unmodified lines
8080 fn update_language_server_status(
8181 &self,
8282 language_server_id: LanguageServerName,
83− status: LanguageServerBinaryStatus,
83+ status: BinaryStatus,
8484 ) {
8585 self.language_registry
8686 .update_lsp_status(language_server_id, status);
crates/languages/src/json.rs+10−1
@@ -86,6 +86,7 @@ impl JsonLspAdapter { · 85 unmodified lines
8686 cx,
8787 );
8888 let tasks_schema = task::TaskTemplates::generate_json_schema();
89+ let debug_schema = task::DebugTaskFile::generate_json_schema();
8990 let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();
9091 let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
9192 let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
@@ -137,7 +138,15 @@ impl JsonLspAdapter { · 45 unmodified lines
137138 )
138139 ],
139140 "schema": snippets_schema,
140− }
141+ },
142+ {
143+ "fileMatch": [
144+ schema_file_match(paths::debug_tasks_file()),
145+ paths::local_debug_file_relative_path()
146+ ],
147+ "schema": debug_schema,
148+
149+ },
141150 ]
142151 }
143152 })
crates/paths/src/paths.rs+24−0
@@ -169,6 +169,12 @@ pub fn tasks_file() -> &'static PathBuf { · 168 unmodified lines
169169 TASKS_FILE.get_or_init(|| config_dir().join("tasks.json"))
170170 }
171171
172+/// Returns the path to the `debug.json` file.
173+pub fn debug_tasks_file() -> &'static PathBuf {
174+ static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
175+ DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json"))
176+}
177+
172178 /// Returns the path to the extensions directory.
173179 ///
174180 /// This is where installed extensions are stored.
@@ -284,6 +290,14 @@ pub fn languages_dir() -> &'static PathBuf { · 109 unmodified lines
284290 LANGUAGES_DIR.get_or_init(|| support_dir().join("languages"))
285291 }
286292
293+/// Returns the path to the debug adapters directory
294+///
295+/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed.
296+pub fn debug_adapters_dir() -> &'static PathBuf {
297+ static DEBUG_ADAPTERS_DIR: OnceLock<PathBuf> = OnceLock::new();
298+ DEBUG_ADAPTERS_DIR.get_or_init(|| support_dir().join("debug_adapters"))
299+}
300+
287301 /// Returns the path to the Copilot directory.
288302 pub fn copilot_dir() -> &'static PathBuf {
289303 static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -328,5 +342,15 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path { · 38 unmodified lines
328342 Path::new(".vscode/tasks.json")
329343 }
330344
345+/// Returns the relative path to a `launch.json` file within a project.
346+pub fn local_debug_file_relative_path() -> &'static Path {
347+ Path::new(".zed/debug.json")
348+}
349+
350+/// Returns the relative path to a `.vscode/launch.json` file within a project.
351+pub fn local_vscode_launch_file_relative_path() -> &'static Path {
352+ Path::new(".vscode/launch.json")
353+}
354+
331355 /// A default editorconfig file name to use when resolving project settings.
332356 pub const EDITORCONFIG_NAME: &str = ".editorconfig";
crates/project/Cargo.toml+10−2
@@ -22,6 +22,8 @@ test-support = [ · 21 unmodified lines
2222 "prettier/test-support",
2323 "worktree/test-support",
2424 "gpui/test-support",
25+ "dap/test-support",
26+ "dap_adapters/test-support",
2527 ]
2628
2729 [dependencies]
@@ -30,9 +32,12 @@ anyhow.workspace = true · 2 unmodified lines
3032 askpass.workspace = true
3133 async-trait.workspace = true
3234 buffer_diff.workspace = true
35+circular-buffer.workspace = true
3336 client.workspace = true
3437 clock.workspace = true
3538 collections.workspace = true
39+dap.workspace = true
40+dap_adapters.workspace = true
3641 extension.workspace = true
3742 fancy-regex.workspace = true
3843 fs.workspace = true
@@ -44,6 +49,7 @@ gpui.workspace = true · 5 unmodified lines
4449 http_client.workspace = true
4550 image.workspace = true
4651 itertools.workspace = true
52+indexmap.workspace = true
4753 language.workspace = true
4854 log.workspace = true
4955 lsp.workspace = true
@@ -81,17 +87,19 @@ worktree.workspace = true · 31 unmodified lines
8187 client = { workspace = true, features = ["test-support"] }
8288 collections = { workspace = true, features = ["test-support"] }
8389 buffer_diff = { workspace = true, features = ["test-support"] }
90+dap = { workspace = true, features = ["test-support"] }
91+dap_adapters = { workspace = true, features = ["test-support"] }
8492 env_logger.workspace = true
8593 fs = { workspace = true, features = ["test-support"] }
8694 git2.workspace = true
8795 gpui = { workspace = true, features = ["test-support"] }
8896 language = { workspace = true, features = ["test-support"] }
89−release_channel.workspace = true
9097 lsp = { workspace = true, features = ["test-support"] }
9198 prettier = { workspace = true, features = ["test-support"] }
9299 pretty_assertions.workspace = true
93−worktree = { workspace = true, features = ["test-support"] }
100+release_channel.workspace = true
94101 rpc = { workspace = true, features = ["test-support"] }
95102 settings = { workspace = true, features = ["test-support"] }
96103 unindent.workspace = true
97104 util = { workspace = true, features = ["test-support"] }
105+worktree = { workspace = true, features = ["test-support"] }
crates/project/src/buffer_store.rs+31−4
@@ -85,6 +85,10 @@ enum OpenBuffer { · 84 unmodified lines
8585
8686 pub enum BufferStoreEvent {
8787 BufferAdded(Entity<Buffer>),
88+ BufferOpened {
89+ buffer: Entity<Buffer>,
90+ project_path: ProjectPath,
91+ },
8892 SharedBufferClosed(proto::PeerId, BufferId),
8993 BufferDropped(BufferId),
9094 BufferChangedFilePath {
@@ -802,6 +806,13 @@ impl BufferStore { · 711 unmodified lines
802806 }
803807 }
804808
809+ fn as_local(&self) -> Option<&LocalBufferStore> {
810+ match &self.state {
811+ BufferStoreState::Local(state) => Some(state),
812+ _ => None,
813+ }
814+ }
815+
805816 fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {
806817 match &mut self.state {
807818 BufferStoreState::Local(state) => Some(state),
@@ -829,6 +840,11 @@ impl BufferStore { · 21 unmodified lines
829840 cx: &mut Context<Self>,
830841 ) -> Task<Result<Entity<Buffer>>> {
831842 if let Some(buffer) = self.get_by_path(&project_path, cx) {
843+ cx.emit(BufferStoreEvent::BufferOpened {
844+ buffer: buffer.clone(),
845+ project_path,
846+ });
847+
832848 return Task::ready(Ok(buffer));
833849 }
834850
@@ -852,12 +868,18 @@ impl BufferStore { · 17 unmodified lines
852868 .insert(
853869 cx.spawn(move |this, mut cx| async move {
854870 let load_result = load_buffer.await;
855− this.update(&mut cx, |this, _cx| {
871+ this.update(&mut cx, |this, cx| {
856872 // Record the fact that the buffer is no longer loading.
857873 this.loading_buffers.remove(&project_path);
858− })
859− .ok();
860− load_result.map_err(Arc::new)
874+
875+ let buffer = load_result.map_err(Arc::new)?;
876+ cx.emit(BufferStoreEvent::BufferOpened {
877+ buffer: buffer.clone(),
878+ project_path,
879+ });
880+
881+ Ok(buffer)
882+ })?
861883 })
862884 .shared(),
863885 )
@@ -1147,6 +1169,11 @@ impl BufferStore { · 283 unmodified lines
11471169 })
11481170 }
11491171
1172+ pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> {
1173+ self.as_local()
1174+ .and_then(|state| state.local_buffer_ids_by_path.get(project_path))
1175+ }
1176+
11501177 pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {
11511178 self.buffers().find_map(|buffer| {
11521179 let file = File::from_dyn(buffer.read(cx).file())?;
crates/project/src/debugger.rs+19−0
@@ -0,0 +1,19 @@
1+//! Zed's debugger data layer is implemented in terms of 3 concepts:
2+//! - DAP store - that knows about all of the available debug sessions.
3+//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session.
4+//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host).
5+//! - Breakpoint store - that knows about all breakpoints set for a project.
6+//!
7+//! There are few reasons for this divide:
8+//! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them
9+//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up.
10+//! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain
11+//! current set of breakpoints.
12+//! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session.
13+
14+pub mod breakpoint_store;
15+pub mod dap_command;
16+pub mod dap_store;
17+pub mod session;
18+
19+pub use dap_adapters::attach_processes;
crates/project/src/debugger/README.md+350−0
@@ -0,0 +1,350 @@
1+# Debugger
2+
3+Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages.
4+DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other.
5+It allows Zed to support various debuggers without needing to implement language-specific debugging logic.
6+This protocol enables features like setting breakpoints, stepping through code, inspecting variables,
7+and more, in a consistent manner across different programming languages and runtime environments.
8+
9+## Supported Debug Adapters
10+
11+Zed supports a variety of debug adapters for different programming languages:
12+
13+- JavaScript (node): Enables debugging of Node.js applications, including setting breakpoints, stepping through code, and inspecting variables in JavaScript.
14+
15+- Python (debugpy): Provides debugging capabilities for Python applications, supporting features like remote debugging, multi-threaded debugging, and Django/Flask application debugging.
16+
17+- LLDB: A powerful debugger for C, C++, Objective-C, and Swift, offering low-level debugging features and support for Apple platforms.
18+
19+- GDB: The GNU Debugger, which supports debugging for multiple programming languages including C, C++, Go, and Rust, across various platforms.
20+
21+- Go (dlv): Delve, a debugger for the Go programming language, offering both local and remote debugging capabilities with full support for Go's runtime and standard library.
22+
23+- PHP (xdebug): Provides debugging and profiling capabilities for PHP applications, including remote debugging and code coverage analysis.
24+
25+- Custom: Allows you to configure any debug adapter that supports the Debug Adapter Protocol, enabling debugging for additional languages or specialized environments not natively supported by Zed.
26+
27+These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger.
28+
29+## How To Get Started
30+
31+To start a debug session, we added few default debug configurations for each supported language that supports generic configuration options. To see all the available debug configurations, you can use the command palette `debugger: start` action, this should list all the available debug configurations.
32+
33+### Configuration
34+
35+To create a custom debug configuration you have to create a `.zed/debug.json` file in your project root directory. This file should contain an array of debug configurations, each with a unique label and adapter the other option are optional/required based on the adapter.
36+
37+```json
38+[
39+ {
40+ // The label for the debug configuration and used to identify the debug session inside the debug panel
41+ "label": "Example Start debugger config"
42+ // The debug adapter that Zed should use to debug the program
43+ "adapter": "custom",
44+ // Request: defaults to launch
45+ // - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration
46+ // - attach: Zed will attach to a running program to debug it or when the process_id is not specified we will show a process picker (only supported for node currently)
47+ "request": "launch",
48+ // cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT)
49+ // this field also supports task variables e.g. $ZED_WORKTREE_ROOT
50+ "cwd": "$ZED_WORKTREE_ROOT",
51+ // program: The program that you want to debug
52+ // this fields also support task variables e.g. $ZED_FILE
53+ // Note: this field should only contain the path to the program you want to debug
54+ "program": "path_to_program",
55+ // initialize_args: This field should contain all the adapter specific initialization arguments that are directly send to the debug adapter
56+ "initialize_args": {
57+ // "stopOnEntry": true // e.g. to stop on the first line of the program (These args are DAP specific)
58+ }
59+ }
60+]
61+```
62+
63+### Using Attach [WIP]
64+
65+Only javascript and lldb supports starting a debug session using attach.
66+
67+When using the attach request with a process ID the syntax is as follows:
68+
69+```json
70+{
71+ "label": "Attach to Process",
72+ "adapter": "javascript",
73+ "request": {
74+ "attach": {
75+ "process_id": "12345"
76+ }
77+ }
78+}
79+```
80+
81+Without process ID the syntax is as follows:
82+
83+```json
84+{
85+ "label": "Attach to Process",
86+ "adapter": "javascript",
87+ "request": {
88+ "attach": {}
89+ }
90+}
91+```
92+
93+#### JavaScript Configuration
94+
95+##### Debug Active File
96+
97+This configuration allows you to debug a JavaScript file in your project.
98+
99+```json
100+{
101+ "label": "JavaScript: Debug Active File",
102+ "adapter": "javascript",
103+ "program": "$ZED_FILE",
104+ "request": "launch",
105+ "cwd": "$ZED_WORKTREE_ROOT"
106+}
107+```
108+
109+##### Debug Terminal
110+
111+This configuration will spawn a debug terminal where you could start you program by typing `node test.js`, and the debug adapter will automatically attach to the process.
112+
113+```json
114+{
115+ "label": "JavaScript: Debug Terminal",
116+ "adapter": "javascript",
117+ "request": "launch",
118+ "cwd": "$ZED_WORKTREE_ROOT",
119+ // "program": "$ZED_FILE", // optional if you pass this in, you will see the output inside the terminal itself
120+ "initialize_args": {
121+ "console": "integratedTerminal"
122+ }
123+}
124+```
125+
126+#### PHP Configuration
127+
128+##### Debug Active File
129+
130+This configuration allows you to debug a PHP file in your project.
131+
132+```json
133+{
134+ "label": "PHP: Debug Active File",
135+ "adapter": "php",
136+ "program": "$ZED_FILE",
137+ "request": "launch",
138+ "cwd": "$ZED_WORKTREE_ROOT"
139+}
140+```
141+

209 more lines aren’t shown here.

crates/project/src/debugger/breakpoint_store.rs+612−0

This diff is large: its 613 lines aren’t shown here.

crates/project/src/debugger/dap_command.rs+1738−0

This diff is large: its 1739 lines aren’t shown here.

crates/project/src/debugger/dap_store.rs+882−0

This diff is large: its 883 lines aren’t shown here.

crates/project/src/debugger/session.rs+1762−0

This diff is large: its 1763 lines aren’t shown here.

crates/project/src/lsp_store.rs+7−11

This diff is large: its 46 lines aren’t shown here.

crates/project/src/project.rs+180−14

This diff is large: its 435 lines aren’t shown here.

crates/project/src/project_settings.rs+32−8

This diff is large: its 113 lines aren’t shown here.

crates/project/src/project_tests.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/project/src/task_inventory.rs+29−11

This diff is large: its 128 lines aren’t shown here.

crates/project/src/task_store.rs+45−12

This diff is large: its 120 lines aren’t shown here.

crates/project/src/terminals.rs+33−0

This diff is large: its 75 lines aren’t shown here.

crates/proto/build.rs+3−0

This diff is large: its 10 lines aren’t shown here.

crates/proto/proto/zed.proto+537−4

This diff is large: its 586 lines aren’t shown here.

crates/proto/src/proto.rs+77−71

This diff is large: its 312 lines aren’t shown here.

crates/recent_projects/Cargo.toml+2−1

This diff is large: its 12 lines aren’t shown here.

crates/recent_projects/src/recent_projects.rs+2−0

This diff is large: its 16 lines aren’t shown here.

crates/remote_server/src/headless_project.rs+34−9

This diff is large: its 91 lines aren’t shown here.

crates/settings/src/settings.rs+5−1

This diff is large: its 17 lines aren’t shown here.

crates/settings/src/settings_store.rs+8−2

This diff is large: its 27 lines aren’t shown here.

crates/sqlez/src/bindable.rs+2−0

This diff is large: its 15 lines aren’t shown here.

crates/sqlez/src/statement.rs+6−0

This diff is large: its 16 lines aren’t shown here.

crates/task/Cargo.toml+8−0

This diff is large: its 27 lines aren’t shown here.

crates/task/src/debug_format.rs+227−0

This diff is large: its 228 lines aren’t shown here.

crates/task/src/lib.rs+32−1

This diff is large: its 61 lines aren’t shown here.

crates/task/src/task_template.rs+91−3

This diff is large: its 136 lines aren’t shown here.

crates/tasks_ui/Cargo.toml+2−0

This diff is large: its 12 lines aren’t shown here.

crates/tasks_ui/src/modal.rs+78−7

This diff is large: its 174 lines aren’t shown here.

crates/tasks_ui/src/tasks_ui.rs+27−8

This diff is large: its 81 lines aren’t shown here.

crates/terminal/src/pty_info.rs+4−0

This diff is large: its 9 lines aren’t shown here.

crates/terminal/src/terminal.rs+7−0

This diff is large: its 35 lines aren’t shown here.

crates/terminal_view/src/terminal_panel.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/terminal_view/src/terminal_view.rs+4−1

This diff is large: its 19 lines aren’t shown here.

crates/text/src/text.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/theme/src/default_colors.rs+5−1

This diff is large: its 34 lines aren’t shown here.

crates/theme/src/fallback_themes.rs+7−0

This diff is large: its 21 lines aren’t shown here.

crates/theme/src/schema.rs+19−2

This diff is large: its 63 lines aren’t shown here.

crates/theme/src/styles/colors.rs+7−2

This diff is large: its 37 lines aren’t shown here.

crates/ui/src/components/button/button_like.rs+42−1

This diff is large: its 75 lines aren’t shown here.

crates/ui/src/components/button/icon_button.rs+9−0

This diff is large: its 23 lines aren’t shown here.

crates/ui/src/components/dropdown_menu.rs+5−1

This diff is large: its 13 lines aren’t shown here.

crates/ui/src/components/icon.rs+13−0

This diff is large: its 20 lines aren’t shown here.

crates/ui/src/components/list/list_item.rs+10−1

This diff is large: its 39 lines aren’t shown here.

crates/ui/src/styles/color.rs+3−0

This diff is large: its 17 lines aren’t shown here.

crates/ui/src/utils/search_input.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/util/src/fs.rs+67−2

This diff is large: its 80 lines aren’t shown here.

crates/vim/src/command.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/workspace/src/pane.rs+19−9

This diff is large: its 87 lines aren’t shown here.

crates/workspace/src/persistence.rs+309−2

This diff is large: its 496 lines aren’t shown here.

crates/workspace/src/persistence/model.rs+3−1

This diff is large: its 21 lines aren’t shown here.

crates/workspace/src/workspace.rs+38−2

This diff is large: its 90 lines aren’t shown here.

crates/zed/Cargo.toml+2−0

This diff is large: its 9 lines aren’t shown here.

crates/zed/src/main.rs+2−0

This diff is large: its 9 lines aren’t shown here.

crates/zed/src/zed.rs+82−58

This diff is large: its 227 lines aren’t shown here.

crates/zeta/src/rate_completion_modal.rs+1−0

This diff is large: its 8 lines aren’t shown here.

docs/src/languages/markdown.md+1−1

This diff is large: its 9 lines aren’t shown here.

docs/src/model-improvement.md+1−1

This diff is large: its 9 lines aren’t shown here.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t