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
13 "futures 0.3.31",13 "futures 0.3.31",
14 "gpui",14 "gpui",
15 "language",15 "language",
16 "lsp",
17 "project",16 "project",
18 "smallvec",17 "smallvec",
19 "ui",18 "ui",
@@ -2644,6 +2643,12 @@ dependencies = [ · 2624 unmodified lines
2644 "zeroize",2643 "zeroize",
2645]2644]
2646 2645
2646[[package]]
2647name = "circular-buffer"
2648version = "1.0.0"
2649source = "registry+https://github.com/rust-lang/crates.io-index"
2650checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f"
2651
2647[[package]]2652[[package]]
2648name = "clang-sys"2653name = "clang-sys"
2649version = "1.8.1"2654version = "1.8.1"
@@ -2893,9 +2898,12 @@ dependencies = [ · 243 unmodified lines
2893 "clock",2898 "clock",
2894 "collab_ui",2899 "collab_ui",
2895 "collections",2900 "collections",
2901 "command_palette_hooks",
2896 "context_server",2902 "context_server",
2897 "ctor",2903 "ctor",
2904 "dap",
2898 "dashmap 6.1.0",2905 "dashmap 6.1.0",
2906 "debugger_ui",
2899 "derive_more",2907 "derive_more",
2900 "editor",2908 "editor",
2901 "env_logger 0.11.7",2909 "env_logger 0.11.7",
@@ -3839,6 +3847,66 @@ dependencies = [ · 937 unmodified lines
3839 "syn 2.0.100",3847 "syn 2.0.100",
3840]3848]
3841 3849
3850[[package]]
3851name = "dap"
3852version = "0.1.0"
3853dependencies = [
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]]
3883name = "dap-types"
3884version = "0.0.1"
3885source = "git+https://github.com/zed-industries/dap-types?rev=bfd4af0#bfd4af084bbaa5f344e6925370d7642e41d0b5b8"
3886dependencies = [
3887 "schemars",
3888 "serde",
3889 "serde_json",
3890]
3891
3892[[package]]
3893name = "dap_adapters"
3894version = "0.1.0"
3895dependencies = [
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
3842[[package]]3910[[package]]
3843name = "dashmap"3911name = "dashmap"
3844version = "5.5.3"3912version = "5.5.3"
@@ -3912,6 +3980,58 @@ dependencies = [ · 67 unmodified lines
3912 "winapi",3980 "winapi",
3913]3981]
3914 3982
3983[[package]]
3984name = "debugger_tools"
3985version = "0.1.0"
3986dependencies = [
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]]
4001name = "debugger_ui"
4002version = "0.1.0"
4003dependencies = [
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
3915[[package]]4035[[package]]
3916name = "deepseek"4036name = "deepseek"
3917version = "0.1.0"4037version = "0.1.0"
@@ -4212,6 +4332,7 @@ dependencies = [ · 294 unmodified lines
4212 "db",4332 "db",
4213 "emojis",4333 "emojis",
4214 "env_logger 0.11.7",4334 "env_logger 0.11.7",
4335 "feature_flags",
4215 "file_icons",4336 "file_icons",
4216 "fs",4337 "fs",
4217 "futures 0.3.31",4338 "futures 0.3.31",
@@ -4228,6 +4349,7 @@ dependencies = [ · 10 unmodified lines
4228 "log",4349 "log",
4229 "lsp",4350 "lsp",
4230 "markdown",4351 "markdown",
4352 "menu",
4231 "multi_buffer",4353 "multi_buffer",
4232 "ordered-float 2.10.1",4354 "ordered-float 2.10.1",
4233 "parking_lot",4355 "parking_lot",
@@ -10422,9 +10544,12 @@ dependencies = [ · 6188 unmodified lines
10422 "askpass",10544 "askpass",
10423 "async-trait",10545 "async-trait",
10424 "buffer_diff",10546 "buffer_diff",
10547 "circular-buffer",
10425 "client",10548 "client",
10426 "clock",10549 "clock",
10427 "collections",10550 "collections",
10551 "dap",
10552 "dap_adapters",
10428 "env_logger 0.11.7",10553 "env_logger 0.11.7",
10429 "extension",10554 "extension",
10430 "fancy-regex 0.14.0",10555 "fancy-regex 0.14.0",
@@ -10437,6 +10562,7 @@ dependencies = [ · 6 unmodified lines
10437 "gpui",10562 "gpui",
10438 "http_client",10563 "http_client",
10439 "image",10564 "image",
10565 "indexmap",
10440 "itertools 0.14.0",10566 "itertools 0.14.0",
10441 "language",10567 "language",
10442 "log",10568 "log",
@@ -11100,6 +11226,7 @@ version = "0.1.0" · 657 unmodified lines
11100dependencies = [11226dependencies = [
11101 "anyhow",11227 "anyhow",
11102 "auto_update",11228 "auto_update",
11229 "dap",
11103 "editor",11230 "editor",
11104 "extension_host",11231 "extension_host",
11105 "file_finder",11232 "file_finder",
@@ -13657,12 +13784,14 @@ version = "0.1.0" · 2551 unmodified lines
13657dependencies = [13784dependencies = [
13658 "anyhow",13785 "anyhow",
13659 "collections",13786 "collections",
13787 "dap-types",
13660 "futures 0.3.31",13788 "futures 0.3.31",
13661 "gpui",13789 "gpui",
13662 "hex",13790 "hex",
13663 "parking_lot",13791 "parking_lot",
13664 "schemars",13792 "schemars",
13665 "serde",13793 "serde",
13794 "serde_json",
13666 "serde_json_lenient",13795 "serde_json_lenient",
13667 "sha2",13796 "sha2",
13668 "shellexpand 2.1.2",13797 "shellexpand 2.1.2",
@@ -13675,7 +13804,9 @@ name = "tasks_ui" · 6 unmodified lines
13675version = "0.1.0"13804version = "0.1.0"
13676dependencies = [13805dependencies = [
13677 "anyhow",13806 "anyhow",
13807 "debugger_ui",
13678 "editor",13808 "editor",
13809 "feature_flags",
13679 "file_icons",13810 "file_icons",
13680 "fuzzy",13811 "fuzzy",
13681 "gpui",13812 "gpui",
@@ -17218,6 +17349,8 @@ dependencies = [ · 3536 unmodified lines
17218 "component_preview",17349 "component_preview",
17219 "copilot",17350 "copilot",
17220 "db",17351 "db",
17352 "debugger_tools",
17353 "debugger_ui",
17221 "diagnostics",17354 "diagnostics",
17222 "editor",17355 "editor",
17223 "env_logger 0.11.7",17356 "env_logger 0.11.7",
Cargo.toml+10−0
@@ -37,6 +37,10 @@ members = [ · 36 unmodified lines
37 "crates/context_server_settings",37 "crates/context_server_settings",
38 "crates/copilot",38 "crates/copilot",
39 "crates/credentials_provider",39 "crates/credentials_provider",
40 "crates/dap",
41 "crates/dap_adapters",
42 "crates/debugger_tools",
43 "crates/debugger_ui",
40 "crates/db",44 "crates/db",
41 "crates/deepseek",45 "crates/deepseek",
42 "crates/diagnostics",46 "crates/diagnostics",
@@ -236,7 +240,11 @@ context_server = { path = "crates/context_server" } · 193 unmodified lines
236context_server_settings = { path = "crates/context_server_settings" }240context_server_settings = { path = "crates/context_server_settings" }
237copilot = { path = "crates/copilot" }241copilot = { path = "crates/copilot" }
238credentials_provider = { path = "crates/credentials_provider" }242credentials_provider = { path = "crates/credentials_provider" }
243dap = { path = "crates/dap" }
244dap_adapters = { path = "crates/dap_adapters" }
239db = { path = "crates/db" }245db = { path = "crates/db" }
246debugger_ui = { path = "crates/debugger_ui" }
247debugger_tools = { path = "crates/debugger_tools" }
240deepseek = { path = "crates/deepseek" }248deepseek = { path = "crates/deepseek" }
241diagnostics = { path = "crates/diagnostics" }249diagnostics = { path = "crates/diagnostics" }
242buffer_diff = { path = "crates/buffer_diff" }250buffer_diff = { path = "crates/buffer_diff" }
@@ -402,6 +410,7 @@ bytes = "1.0" · 159 unmodified lines
402cargo_metadata = "0.19"410cargo_metadata = "0.19"
403cargo_toml = "0.21"411cargo_toml = "0.21"
404chrono = { version = "0.4", features = ["serde"] }412chrono = { version = "0.4", features = ["serde"] }
413circular-buffer = "1.0"
405clap = { version = "4.4", features = ["derive"] }414clap = { version = "4.4", features = ["derive"] }
406cocoa = "0.26"415cocoa = "0.26"
407cocoa-foundation = "0.2.0"416cocoa-foundation = "0.2.0"
@@ -410,6 +419,7 @@ core-foundation = "0.9.3" · 2 unmodified lines
410core-foundation-sys = "0.8.6"419core-foundation-sys = "0.8.6"
411ctor = "0.4.0"420ctor = "0.4.0"
412dashmap = "6.0"421dashmap = "6.0"
422dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" }
413derive_more = "0.99.17"423derive_more = "0.99.17"
414dirs = "4.0"424dirs = "4.0"
415ec4rs = "1.1"425ec4rs = "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
30 "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],30 "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
31 "ctrl-,": "zed::OpenSettings",31 "ctrl-,": "zed::OpenSettings",
32 "ctrl-q": "zed::Quit",32 "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",
33 "f11": "zed::ToggleFullScreen",40 "f11": "zed::ToggleFullScreen",
34 "ctrl-alt-z": "edit_prediction::RateCompletions",41 "ctrl-alt-z": "edit_prediction::RateCompletions",
35 "ctrl-shift-i": "edit_prediction::ToggleMenu"42 "ctrl-shift-i": "edit_prediction::ToggleMenu"
@@ -124,7 +131,9 @@ · 88 unmodified lines
124 "alt-g b": "editor::ToggleGitBlame",131 "alt-g b": "editor::ToggleGitBlame",
125 "menu": "editor::OpenContextMenu",132 "menu": "editor::OpenContextMenu",
126 "shift-f10": "editor::OpenContextMenu",133 "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"
128 }137 }
129 },138 },
130 {139 {
assets/keymaps/default-macos.json+17−0
@@ -14,6 +14,13 @@ · 13 unmodified lines
14 {14 {
15 "use_key_equivalents": true,15 "use_key_equivalents": true,
16 "bindings": {16 "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",
17 "home": "menu::SelectFirst",24 "home": "menu::SelectFirst",
18 "shift-pageup": "menu::SelectFirst",25 "shift-pageup": "menu::SelectFirst",
19 "pageup": "menu::SelectFirst",26 "pageup": "menu::SelectFirst",
@@ -148,6 +155,8 @@ · 128 unmodified lines
148 "cmd-\"": "editor::ExpandAllDiffHunks",155 "cmd-\"": "editor::ExpandAllDiffHunks",
149 "cmd-alt-g b": "editor::ToggleGitBlame",156 "cmd-alt-g b": "editor::ToggleGitBlame",
150 "cmd-i": "editor::ShowSignatureHelp",157 "cmd-i": "editor::ShowSignatureHelp",
158 "f9": "editor::ToggleBreakpoint",
159 "shift-f9": "editor::EditLogBreakpoint",
151 "ctrl-f12": "editor::GoToDeclaration",160 "ctrl-f12": "editor::GoToDeclaration",
152 "alt-ctrl-f12": "editor::GoToDeclarationSplit",161 "alt-ctrl-f12": "editor::GoToDeclarationSplit",
153 "ctrl-cmd-e": "editor::ToggleEditPrediction"162 "ctrl-cmd-e": "editor::ToggleEditPrediction"
@@ -756,6 +765,14 @@ · 602 unmodified lines
756 "space": "project_panel::Open"765 "space": "project_panel::Open"
757 }766 }
758 },767 },
768 {
769 "context": "VariableList",
770 "use_key_equivalents": true,
771 "bindings": {
772 "left": "variable_list::CollapseSelectedEntry",
773 "right": "variable_list::ExpandSelectedEntry"
774 }
775 },
759 {776 {
760 "context": "GitPanel && ChangesList",777 "context": "GitPanel && ChangesList",
761 "use_key_equivalents": true,778 "use_key_equivalents": true,
assets/keymaps/linux/jetbrains.json+11−2
@@ -3,7 +3,14 @@ · 2 unmodified lines
3 "bindings": {3 "bindings": {
4 "ctrl-alt-s": "zed::OpenSettings",4 "ctrl-alt-s": "zed::OpenSettings",
5 "ctrl-{": "pane::ActivatePreviousItem",5 "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"
7 }14 }
8 },15 },
9 {16 {
@@ -49,7 +56,9 @@ · 39 unmodified lines
49 "ctrl-home": "editor::MoveToBeginning",56 "ctrl-home": "editor::MoveToBeginning",
50 "ctrl-end": "editor::MoveToEnd",57 "ctrl-end": "editor::MoveToEnd",
51 "ctrl-shift-home": "editor::SelectToBeginning",58 "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"
53 }62 }
54 },63 },
55 {64 {
assets/keymaps/macos/jetbrains.json+11−2
@@ -2,7 +2,14 @@ · 1 unmodified line
2 {2 {
3 "bindings": {3 "bindings": {
4 "cmd-{": "pane::ActivatePreviousItem",4 "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"
6 }13 }
7 },14 },
8 {15 {
@@ -46,7 +53,9 @@ · 37 unmodified lines
46 "cmd-home": "editor::MoveToBeginning",53 "cmd-home": "editor::MoveToBeginning",
47 "cmd-end": "editor::MoveToEnd",54 "cmd-end": "editor::MoveToEnd",
48 "cmd-shift-home": "editor::SelectToBeginning",55 "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"
50 }59 }
51 },60 },
52 {61 {
assets/settings/default.json+9−1
@@ -324,6 +324,8 @@ · 323 unmodified lines
324 "code_actions": true,324 "code_actions": true,
325 // Whether to show runnables buttons in the gutter.325 // Whether to show runnables buttons in the gutter.
326 "runnables": true,326 "runnables": true,
327 // Whether to show breakpoints in the gutter.
328 "breakpoints": true,
327 // Whether to show fold buttons in the gutter.329 // Whether to show fold buttons in the gutter.
328 "folds": true330 "folds": true
329 },331 },
@@ -1453,6 +1455,12 @@ · 1123 unmodified lines
1453 // }1455 // }
1454 // ]1456 // ]
1455 "ssh_connections": [],1457 "ssh_connections": [],
1458
1456 // Configures context servers for use in the Assistant.1459 // 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 }
1458}1466}
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
20futures.workspace = true20futures.workspace = true
21gpui.workspace = true21gpui.workspace = true
22language.workspace = true22language.workspace = true
23lsp.workspace = true
24project.workspace = true23project.workspace = true
25smallvec.workspace = true24smallvec.workspace = true
26ui.workspace = true25ui.workspace = true
crates/activity_indicator/src/activity_indicator.rs+32−22
@@ -7,8 +7,7 @@ use gpui::{ · 6 unmodified lines
7 EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,7 EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
8 StatefulInteractiveElement, Styled, Transformation, Window,8 StatefulInteractiveElement, Styled, Transformation, Window,
9};9};
10use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};10use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
11use lsp::LanguageServerName;
12use project::{11use project::{
13 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,12 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
14 ProjectEnvironmentEvent, WorktreeId,13 ProjectEnvironmentEvent, WorktreeId,
@@ -23,21 +22,21 @@ actions!(activity_indicator, [ShowErrorMessage]); · 8 unmodified lines
23 22
24pub enum Event {23pub enum Event {
25 ShowError {24 ShowError {
26 lsp_name: LanguageServerName,25 server_name: SharedString,
27 error: String,26 error: String,
28 },27 },
29}28}
30 29
31pub struct ActivityIndicator {30pub struct ActivityIndicator {
32 statuses: Vec<LspStatus>,31 statuses: Vec<ServerStatus>,
33 project: Entity<Project>,32 project: Entity<Project>,
34 auto_updater: Option<Entity<AutoUpdater>>,33 auto_updater: Option<Entity<AutoUpdater>>,
35 context_menu_handle: PopoverMenuHandle<ContextMenu>,34 context_menu_handle: PopoverMenuHandle<ContextMenu>,
36}35}
37 36
38struct LspStatus {37struct ServerStatus {
39 name: LanguageServerName,38 name: SharedString,
40 status: LanguageServerBinaryStatus,39 status: BinaryStatus,
41}40}
42 41
43struct PendingWork<'a> {42struct PendingWork<'a> {
@@ -68,7 +67,20 @@ impl ActivityIndicator { · 24 unmodified lines
68 while let Some((name, status)) = status_events.next().await {67 while let Some((name, status)) = status_events.next().await {
69 this.update(&mut cx, |this: &mut ActivityIndicator, cx| {68 this.update(&mut cx, |this: &mut ActivityIndicator, cx| {
70 this.statuses.retain(|s| s.name != name);69 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 });
72 cx.notify();84 cx.notify();
73 })?;85 })?;
74 }86 }
@@ -106,18 +118,18 @@ impl ActivityIndicator { · 31 unmodified lines
106 });118 });
107 119
108 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {120 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
109 Event::ShowError { lsp_name, error } => {121 Event::ShowError { server_name, error } => {
110 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));122 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
111 let project = project.clone();123 let project = project.clone();
112 let error = error.clone();124 let error = error.clone();
113 let lsp_name = lsp_name.clone();125 let server_name = server_name.clone();
114 cx.spawn_in(window, |workspace, mut cx| async move {126 cx.spawn_in(window, |workspace, mut cx| async move {
115 let buffer = create_buffer.await?;127 let buffer = create_buffer.await?;
116 buffer.update(&mut cx, |buffer, cx| {128 buffer.update(&mut cx, |buffer, cx| {
117 buffer.edit(129 buffer.edit(
118 [(130 [(
119 0..0,131 0..0,
120 format!("Language server error: {}\n\n{}", lsp_name, error),132 format!("Language server error: {}\n\n{}", server_name, error),
121 )],133 )],
122 None,134 None,
123 cx,135 cx,
@@ -147,9 +159,9 @@ impl ActivityIndicator { · 23 unmodified lines
147 159
148 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {160 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
149 self.statuses.retain(|status| {161 self.statuses.retain(|status| {
150 if let LanguageServerBinaryStatus::Failed { error } = &status.status {162 if let BinaryStatus::Failed { error } = &status.status {
151 cx.emit(Event::ShowError {163 cx.emit(Event::ShowError {
152 lsp_name: status.name.clone(),164 server_name: status.name.clone(),
153 error: error.clone(),165 error: error.clone(),
154 });166 });
155 false167 false
@@ -278,12 +290,10 @@ impl ActivityIndicator { · 122 unmodified lines
278 let mut failed = SmallVec::<[_; 3]>::new();290 let mut failed = SmallVec::<[_; 3]>::new();
279 for status in &self.statuses {291 for status in &self.statuses {
280 match status.status {292 match status.status {
281 LanguageServerBinaryStatus::CheckingForUpdate => {293 BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
282 checking_for_update.push(status.name.clone())294 BinaryStatus::Downloading => downloading.push(status.name.clone()),
283 }295 BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
284 LanguageServerBinaryStatus::Downloading => downloading.push(status.name.clone()),296 BinaryStatus::None => {}
285 LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.clone()),
286 LanguageServerBinaryStatus::None => {}
287 }297 }
288 }298 }
289 299
@@ -296,7 +306,7 @@ impl ActivityIndicator { · 6 unmodified lines
296 ),306 ),
297 message: format!(307 message: format!(
298 "Downloading {}...",308 "Downloading {}...",
299 downloading.iter().map(|name| name.0.as_ref()).fold(309 downloading.iter().map(|name| name.as_ref()).fold(
300 String::new(),310 String::new(),
301 |mut acc, s| {311 |mut acc, s| {
302 if !acc.is_empty() {312 if !acc.is_empty() {
@@ -324,7 +334,7 @@ impl ActivityIndicator { · 21 unmodified lines
324 ),334 ),
325 message: format!(335 message: format!(
326 "Checking for updates to {}...",336 "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(
328 String::new(),338 String::new(),
329 |mut acc, s| {339 |mut acc, s| {
330 if !acc.is_empty() {340 if !acc.is_empty() {
@@ -354,7 +364,7 @@ impl ActivityIndicator { · 23 unmodified lines
354 "Failed to run {}. Click to show error.",364 "Failed to run {}. Click to show error.",
355 failed365 failed
356 .iter()366 .iter()
357 .map(|name| name.0.as_ref())367 .map(|name| name.as_ref())
358 .fold(String::new(), |mut acc, s| {368 .fold(String::new(), |mut acc, s| {
359 if !acc.is_empty() {369 if !acc.is_empty() {
360 acc.push_str(", ");370 acc.push_str(", ");
crates/assistant_context_editor/src/context_editor.rs+1−0
@@ -229,6 +229,7 @@ impl ContextEditor { · 228 unmodified lines
229 editor.set_show_git_diff_gutter(false, cx);229 editor.set_show_git_diff_gutter(false, cx);
230 editor.set_show_code_actions(false, cx);230 editor.set_show_code_actions(false, cx);
231 editor.set_show_runnables(false, cx);231 editor.set_show_runnables(false, cx);
232 editor.set_show_breakpoints(false, cx);
232 editor.set_show_wrap_guides(false, cx);233 editor.set_show_wrap_guides(false, cx);
233 editor.set_show_indent_guides(false, cx);234 editor.set_show_indent_guides(false, cx);
234 editor.set_completion_provider(Some(Box::new(completion_provider)));235 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
89client = { workspace = true, features = ["test-support"] }89client = { workspace = true, features = ["test-support"] }
90collab_ui = { workspace = true, features = ["test-support"] }90collab_ui = { workspace = true, features = ["test-support"] }
91collections = { workspace = true, features = ["test-support"] }91collections = { workspace = true, features = ["test-support"] }
92command_palette_hooks.workspace = true
92context_server.workspace = true93context_server.workspace = true
93ctor.workspace = true94ctor.workspace = true
95dap = { workspace = true, features = ["test-support"] }
96debugger_ui = { workspace = true, features = ["test-support"] }
94editor = { workspace = true, features = ["test-support"] }97editor = { workspace = true, features = ["test-support"] }
95env_logger.workspace = true98env_logger.workspace = true
96extension.workspace = true99extension.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
469);469);
470 470
471CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);471CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
472
473CREATE 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);
482CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
crates/collab/migrations/20241121185750_add_breakpoints.sql+11−0
@@ -0,0 +1,11 @@
1CREATE 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
11CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
crates/collab/src/db/queries/projects.rs+40−29
@@ -1,5 +1,5 @@
1use anyhow::Context as _;1use anyhow::Context as _;
2 2use collections::HashSet;
3use util::ResultExt;3use util::ResultExt;
4 4
5use super::*;5use super::*;
@@ -1106,39 +1106,50 @@ impl Database { · 1100 unmodified lines
1106 exclude_dev_server: bool,1106 exclude_dev_server: bool,
1107 ) -> Result<TransactionGuard<HashSet<ConnectionId>>> {1107 ) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
1108 self.project_transaction(project_id, |tx| async move {1108 self.project_transaction(project_id, |tx| async move {
1109 let project = project::Entity::find_by_id(project_id)1109 self.internal_project_connection_ids(project_id, connection_id, exclude_dev_server, &tx)
1110 .one(&*tx)1110 .await
1111 .await?1111 })
1112 .ok_or_else(|| anyhow!("no such project"))?;1112 .await
1113 }
1113 1114
1114 let mut collaborators = project_collaborator::Entity::find()1115 async fn internal_project_connection_ids(
1115 .filter(project_collaborator::Column::ProjectId.eq(project_id))1116 &self,
1116 .stream(&*tx)1117 project_id: ProjectId,
1117 .await?;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"))?;
1118 1126
1119 let mut connection_ids = HashSet::default();1127 let mut collaborators = project_collaborator::Entity::find()
1120 if let Some(host_connection) = project.host_connection().log_err() {1128 .filter(project_collaborator::Column::ProjectId.eq(project_id))
1121 if !exclude_dev_server {1129 .stream(tx)
1122 connection_ids.insert(host_connection);1130 .await?;
1123 }
1124 }
1125 1131
1126 while let Some(collaborator) = collaborators.next().await {1132 let mut connection_ids = HashSet::default();
1127 let collaborator = collaborator?;1133 if let Some(host_connection) = project.host_connection().log_err() {
1128 connection_ids.insert(collaborator.connection());1134 if !exclude_dev_server {
1135 connection_ids.insert(host_connection);
1129 }1136 }
1137 }
1130 1138
1131 if connection_ids.contains(&connection_id)1139 while let Some(collaborator) = collaborators.next().await {
1132 || Some(connection_id) == project.host_connection().ok()1140 let collaborator = collaborator?;
1133 {1141 connection_ids.insert(collaborator.connection());
1134 Ok(connection_ids)1142 }
1135 } else {1143
1136 Err(anyhow!(1144 if connection_ids.contains(&connection_id)
1137 "can only send project updates to a project you're in"1145 || Some(connection_id) == project.host_connection().ok()
1138 ))?1146 {
1139 }1147 Ok(connection_ids)
1140 })1148 } else {
1141 .await1149 Err(anyhow!(
1150 "can only send project updates to a project you're in"
1151 ))?
1152 }
1142 }1153 }
1143 1154
1144 async fn project_guest_connection_ids(1155 async fn project_guest_connection_ids(
crates/collab/src/rpc.rs+3−1
@@ -404,6 +404,8 @@ impl Server { · 403 unmodified lines
404 .add_request_handler(forward_read_only_project_request::<proto::GitReset>)404 .add_request_handler(forward_read_only_project_request::<proto::GitReset>)
405 .add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)405 .add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
406 .add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)406 .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>)
407 .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)409 .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
408 .add_request_handler(forward_mutating_project_request::<proto::GitDiff>)410 .add_request_handler(forward_mutating_project_request::<proto::GitDiff>)
409 .add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)411 .add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
@@ -2064,7 +2066,7 @@ async fn update_worktree_settings( · 1654 unmodified lines
2064 Ok(())2066 Ok(())
2065}2067}
2066 2068
2067/// Notify other participants that a language server has started.2069/// Notify other participants that a language server has started.
2068async fn start_language_server(2070async fn start_language_server(
2069 request: proto::StartLanguageServer,2071 request: proto::StartLanguageServer,
2070 session: Session,2072 session: Session,
crates/collab/src/tests.rs+1−0
@@ -11,6 +11,7 @@ mod channel_buffer_tests; · 10 unmodified lines
11mod channel_guest_tests;11mod channel_guest_tests;
12mod channel_message_tests;12mod channel_message_tests;
13mod channel_tests;13mod channel_tests;
14// mod debug_panel_tests;
14mod editor_tests;15mod editor_tests;
15mod following_tests;16mod following_tests;
16mod git_tests;17mod git_tests;
crates/collab/src/tests/debug_panel_tests.rs+2454−0
@@ -0,0 +1,2454 @@
1use call::ActiveCall;
2use dap::requests::{Initialize, Launch, StackTrace};
3use dap::DebugRequestType;
4use dap::{requests::SetBreakpoints, SourceBreakpoint};
5use debugger_ui::debugger_panel::DebugPanel;
6use debugger_ui::session::DebugSession;
7use editor::Editor;
8use gpui::{Entity, TestAppContext, VisualTestContext};
9use project::{Project, ProjectPath, WorktreeId};
10use serde_json::json;
11use std::sync::Arc;
12use std::{
13 path::Path,
14 sync::atomic::{AtomicBool, Ordering},
15};
16use workspace::{dock::Panel, Workspace};
17
18use super::{TestClient, TestServer};
19
20pub 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
36async 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
49pub 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
61struct ZedInstance<'a> {
62 client: TestClient,
63 project: Option<Entity<Project>>,
64 active_call: Entity<ActiveCall>,
65 cx: &'a mut TestAppContext,
66}
67
68impl<'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
134async 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
165async 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]
187async 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]
272async 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]
373async 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]
703async 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]
836async 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]
1059async 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]
1723async 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]
2163async 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
21};21};
22use project::{22use project::{
23 project_settings::{InlineBlameSettings, ProjectSettings},23 project_settings::{InlineBlameSettings, ProjectSettings},
24 SERVER_PROGRESS_THROTTLE_TIMEOUT,24 ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
25};25};
26use recent_projects::disconnected_overlay::DisconnectedOverlay;26use recent_projects::disconnected_overlay::DisconnectedOverlay;
27use rpc::RECEIVE_TIMEOUT;27use rpc::RECEIVE_TIMEOUT;
@@ -2408,6 +2408,209 @@ fn main() { let foo = other::foo(); }"}; · 2380 unmodified lines
2408 );2408 );
2409}2409}
2410 2410
2411#[gpui::test]
2412async 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
2411#[track_caller]2614#[track_caller]
2412fn tab_undo_assert(2615fn tab_undo_assert(
2413 cx_a: &mut EditorTestContext,2616 cx_a: &mut EditorTestContext,
crates/dap/Cargo.toml+53−0
@@ -0,0 +1,53 @@
1[package]
2name = "dap"
3version = "0.1.0"
4edition.workspace = true
5publish.workspace = true
6license = "GPL-3.0-or-later"
7
8[lints]
9workspace = true
10
11[features]
12test-support = [
13 "gpui/test-support",
14 "util/test-support",
15 "task/test-support",
16 "async-pipe",
17 "settings/test-support",
18]
19
20[dependencies]
21anyhow.workspace = true
22async-compression.workspace = true
23async-pipe = { workspace = true, optional = true }
24async-tar.workspace = true
25async-trait.workspace = true
26client.workspace = true
27collections.workspace = true
28dap-types.workspace = true
29fs.workspace = true
30futures.workspace = true
31gpui.workspace = true
32http_client.workspace = true
33language.workspace = true
34log.workspace = true
35node_runtime.workspace = true
36parking_lot.workspace = true
37paths.workspace = true
38schemars.workspace = true
39serde.workspace = true
40serde_json.workspace = true
41settings.workspace = true
42smallvec.workspace = true
43smol.workspace = true
44task.workspace = true
45util.workspace = true
46
47[dev-dependencies]
48async-pipe.workspace = true
49env_logger.workspace = true
50gpui = { workspace = true, features = ["test-support"] }
51settings = { workspace = true, features = ["test-support"] }
52task = { workspace = true, features = ["test-support"] }
53util = { 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
3The active `Project` is responsible for maintain opened and closed breakpoints
4as well as serializing breakpoints to save. At a high level project serializes
5the positions of breakpoints that don't belong to any active buffers and handles
6converting 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
9during debugging or when starting a debugger.
crates/dap/src/adapters.rs+370−0
@@ -0,0 +1,370 @@
1use ::fs::Fs;
2use anyhow::{anyhow, Context as _, Ok, Result};
3use async_compression::futures::bufread::GzipDecoder;
4use async_tar::Archive;
5use async_trait::async_trait;
6use futures::io::BufReader;
7use gpui::{AsyncApp, SharedString};
8pub use http_client::{github::latest_github_release, HttpClient};
9use language::LanguageToolchainStore;
10use node_runtime::NodeRuntime;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use settings::WorktreeId;
14use smol::{self, fs::File, lock::Mutex};
15use 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};
24use task::DebugAdapterConfig;
25use util::ResultExt;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum DapStatus {
29 None,
30 CheckingForUpdate,
31 Downloading,
32 Failed { error: String },
33}
34
35#[async_trait(?Send)]
36pub 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)]
49pub struct DebugAdapterName(pub Arc<str>);
50
51impl Deref for DebugAdapterName {
52 type Target = str;
53
54 fn deref(&self) -> &Self::Target {
55 &self.0
56 }
57}
58
59impl AsRef<str> for DebugAdapterName {
60 fn as_ref(&self) -> &str {
61 &self.0
62 }
63}
64
65impl AsRef<Path> for DebugAdapterName {
66 fn as_ref(&self) -> &Path {
67 Path::new(&*self.0)
68 }
69}
70
71impl 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
77impl From<DebugAdapterName> for SharedString {
78 fn from(name: DebugAdapterName) -> Self {
79 SharedString::from(name.0)
80 }
81}
82
83impl<'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)]
90pub struct TcpArguments {
91 pub host: Ipv4Addr,
92 pub port: u16,
93 pub timeout: Option<u64>,
94}
95#[derive(Debug, Clone)]
96pub 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
104pub struct AdapterVersion {
105 pub tag_name: String,
106 pub url: String,
107}
108
109pub enum DownloadedFileType {
110 Vsix,
111 GzipTar,
112 Zip,
113}
114
115pub struct GithubRepo {
116 pub repo_name: String,
117 pub repo_owner: String,
118}
119
120pub 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
196pub 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)]
215pub 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"))]
295pub struct FakeAdapter {}
296
297#[cfg(any(test, feature = "test-support"))]
298impl 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)]
308impl 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 @@
1use crate::{
2 adapters::{DebugAdapterBinary, DebugAdapterName},
3 transport::{IoKind, LogKind, TransportDelegate},
4};
5use anyhow::{anyhow, Result};
6use dap_types::{
7 messages::{Message, Response},
8 requests::Request,
9};
10use futures::{channel::oneshot, select, FutureExt as _};
11use gpui::{AppContext, AsyncApp, BackgroundExecutor};
12use smol::channel::{Receiver, Sender};
13use std::{
14 hash::Hash,
15 sync::atomic::{AtomicU64, Ordering},
16 time::Duration,
17};
18
19#[cfg(any(test, feature = "test-support"))]
20const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
21
22#[cfg(not(any(test, feature = "test-support")))]
23const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26#[repr(transparent)]
27pub struct SessionId(pub u32);
28
29impl 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.
40pub struct DebugAdapterClient {
41 id: SessionId,
42 name: DebugAdapterName,
43 sequence_count: AtomicU64,
44 binary: DebugAdapterBinary,
45 executor: BackgroundExecutor,
46 transport_delegate: TransportDelegate,
47}
48
49pub type DapMessageHandler = Box<dyn FnMut(Message) + 'static + Send + Sync>;
50
51impl 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)]
283mod 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 @@
1use dap_types::SteppingGranularity;
2use gpui::{App, Global};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use settings::{Settings, SettingsSources};
6
7#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
8#[serde(default)]
9pub 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
36impl 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
49impl 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
59impl Global for DebuggerSettings {}
crates/dap/src/lib.rs+38−0
@@ -0,0 +1,38 @@
1pub mod adapters;
2pub mod client;
3pub mod debugger_settings;
4pub mod proto_conversions;
5pub mod transport;
6
7pub use dap_types::*;
8pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
9
10pub type ScopeId = u64;
11pub type VariableReference = u64;
12pub type StackFrameId = u64;
13
14#[cfg(any(test, feature = "test-support"))]
15pub use adapters::FakeAdapter;
16
17#[cfg(any(test, feature = "test-support"))]
18pub 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 @@
1use anyhow::{anyhow, Result};
2use client::proto::{
3 self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
4 DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
5};
6use dap_types::{OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source};
7
8pub 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
16impl<T> ProtoConversion for Vec<T>
17where
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
35impl 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
78impl 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
112impl 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
138impl 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
165impl 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
184impl 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
208impl 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
250impl 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
287impl 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
337impl 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
358impl 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
383impl 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
423impl 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
446impl 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
485impl 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
521impl 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
574impl 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 @@
1use anyhow::{anyhow, bail, Context, Result};
2use dap_types::{
3 messages::{Message, Response},
4 ErrorResponse,
5};
6use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _};
7use gpui::AsyncApp;
8use settings::Settings as _;
9use smallvec::SmallVec;
10use smol::{
11 channel::{unbounded, Receiver, Sender},
12 io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
13 lock::Mutex,
14 net::{TcpListener, TcpStream},
15 process::Child,
16};
17use std::{
18 collections::HashMap,
19 net::{Ipv4Addr, SocketAddrV4},
20 process::Stdio,
21 sync::Arc,
22 time::Duration,
23};
24use task::TCPHost;
25use util::ResultExt as _;
26
27use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
28
29pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
30
31#[derive(PartialEq, Eq, Clone, Copy)]
32pub enum LogKind {
33 Adapter,
34 Rpc,
35}
36
37pub enum IoKind {
38 StdIn,
39 StdOut,
40 StdErr,
41}
42
43pub 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
50impl 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
66type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
67type LogHandlers = Arc<parking_lot::Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
68
69pub enum Transport {
70 Stdio(StdioTransport),
71 Tcp(TcpTransport),
72 #[cfg(any(test, feature = "test-support"))]
73 Fake(FakeTransport),
74}
75
76impl 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
125pub(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
133impl 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
522pub struct TcpTransport {
523 pub port: u16,
524 pub host: Ipv4Addr,
525 pub timeout: u64,
526 process: Mutex<Child>,
527}
528
529impl 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
633pub struct StdioTransport {
634 process: Mutex<Child>,
635}
636
637impl 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"))]
710type 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"))]
720type ResponseHandler = Box<dyn Send + Fn(Response)>;
721
722#[cfg(any(test, feature = "test-support"))]
723pub 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"))]
731impl 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]
2name = "dap_adapters"
3version = "0.1.0"
4edition.workspace = true
5publish.workspace = true
6license = "GPL-3.0-or-later"
7
8[features]
9test-support = [
10 "dap/test-support",
11 "gpui/test-support",
12 "task/test-support",
13 "util/test-support",
14]
15
16[lints]
17workspace = true
18
19[lib]
20path = "src/dap_adapters.rs"
21doctest = false
22
23[dependencies]
24anyhow.workspace = true
25async-trait.workspace = true
26dap.workspace = true
27gpui.workspace = true
28language.workspace = true
29paths.workspace = true
30regex.workspace = true
31serde.workspace = true
32serde_json.workspace = true
33sysinfo.workspace = true
34task.workspace = true
35util.workspace = true
36
37[dev-dependencies]
38dap = { workspace = true, features = ["test-support"] }
39gpui = { workspace = true, features = ["test-support"] }
40task = { workspace = true, features = ["test-support"] }
41util = { 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 @@
1use dap::transport::TcpTransport;
2use gpui::AsyncApp;
3use serde_json::Value;
4use std::{collections::HashMap, ffi::OsString, path::PathBuf};
5use sysinfo::{Pid, Process};
6use task::DebugAdapterConfig;
7
8use crate::*;
9
10pub(crate) struct CustomDebugAdapter {
11 custom_args: CustomArgs,
12}
13
14impl 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)]
27impl 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 @@
1mod custom;
2#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
3mod gdb;
4mod go;
5mod javascript;
6mod lldb;
7mod php;
8mod python;
9
10use std::{collections::HashMap, sync::Arc};
11
12use anyhow::{anyhow, bail, Result};
13use async_trait::async_trait;
14use custom::CustomDebugAdapter;
15use dap::adapters::{
16 self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
17 GithubRepo,
18};
19#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
20use gdb::GdbDebugAdapter;
21use go::GoDebugAdapter;
22use javascript::JsDebugAdapter;
23use lldb::LldbDebugAdapter;
24use php::PhpDebugAdapter;
25use python::PythonDebugAdapter;
26use serde_json::{json, Value};
27use sysinfo::{Pid, Process};
28use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
29
30pub 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
52pub 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 @@
1use std::ffi::OsStr;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use gpui::AsyncApp;
6use task::DebugAdapterConfig;
7
8use crate::*;
9
10pub(crate) struct GdbDebugAdapter {}
11
12impl GdbDebugAdapter {
13 const ADAPTER_NAME: &'static str = "gdb";
14
15 pub(crate) fn new() -> Self {
16 GdbDebugAdapter {}
17 }
18}
19
20#[async_trait(?Send)]
21impl 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 @@
1use dap::transport::TcpTransport;
2use gpui::AsyncApp;
3use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
4
5use crate::*;
6
7pub(crate) struct GoDebugAdapter {
8 port: u16,
9 host: Ipv4Addr,
10 timeout: Option<u64>,
11}
12
13impl 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)]
26impl 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 @@
1use adapters::latest_github_release;
2use dap::transport::TcpTransport;
3use gpui::AsyncApp;
4use regex::Regex;
5use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf};
6use sysinfo::{Pid, Process};
7use task::DebugRequestType;
8
9use crate::*;
10
11pub(crate) struct JsDebugAdapter {
12 port: u16,
13 host: Ipv4Addr,
14 timeout: Option<u64>,
15}
16
17impl 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)]
40impl 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 @@
1use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use gpui::AsyncApp;
6use sysinfo::{Pid, Process};
7use task::{DebugAdapterConfig, DebugRequestType};
8
9use crate::*;
10
11pub(crate) struct LldbDebugAdapter {}
12
13impl 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)]
26impl 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 @@
1use adapters::latest_github_release;
2use dap::{adapters::TcpArguments, transport::TcpTransport};
3use gpui::AsyncApp;
4use std::{net::Ipv4Addr, path::PathBuf};
5
6use crate::*;
7
8pub(crate) struct PhpDebugAdapter {
9 port: u16,
10 host: Ipv4Addr,
11 timeout: Option<u64>,
12}
13
14impl 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)]
28impl 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 @@
1use crate::*;
2use dap::transport::TcpTransport;
3use gpui::AsyncApp;
4use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
5
6pub(crate) struct PythonDebugAdapter {
7 port: u16,
8 host: Ipv4Addr,
9 timeout: Option<u64>,
10}
11
12impl 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)]
27impl 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]
2name = "debugger_tools"
3version = "0.1.0"
4edition.workspace = true
5publish.workspace = true
6license = "GPL-3.0-or-later"
7
8[lints]
9workspace = true
10
11[lib]
12path = "src/debugger_tools.rs"
13doctest = false
14
15[dependencies]
16anyhow.workspace = true
17dap.workspace = true
18editor.workspace = true
19futures.workspace = true
20gpui.workspace = true
21project.workspace = true
22serde_json.workspace = true
23settings.workspace = true
24smol.workspace = true
25util.workspace = true
26workspace.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 @@
1use dap::{
2 client::SessionId,
3 debugger_settings::DebuggerSettings,
4 transport::{IoKind, LogKind},
5};
6use editor::{Editor, EditorEvent};
7use futures::{
8 channel::mpsc::{unbounded, UnboundedSender},
9 StreamExt,
10};
11use gpui::{
12 actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable,
13 IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
14};
15use project::{
16 debugger::{dap_store, session::Session},
17 search::SearchQuery,
18 Project,
19};
20use settings::Settings as _;
21use std::{
22 borrow::Cow,
23 collections::{HashMap, VecDeque},
24 sync::Arc,
25};
26use util::maybe;
27use 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
34struct 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
44struct 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
51struct ProjectState {
52 _subscriptions: [gpui::Subscription; 2],
53}
54
55struct DebugAdapterState {
56 log_messages: VecDeque<String>,
57 rpc_messages: RpcMessages,
58}
59
60struct RpcMessages {
61 messages: VecDeque<String>,
62 last_message_kind: Option<MessageKind>,
63}
64
65impl 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
76const SEND: &str = "// Send";
77const RECEIVE: &str = "// Receive";
78
79#[derive(Clone, Copy, PartialEq, Eq)]
80enum MessageKind {
81 Send,
82 Receive,
83}
84
85impl MessageKind {
86 fn label(&self) -> &'static str {
87 match self {
88 Self::Send => SEND,
89 Self::Receive => RECEIVE,
90 }
91 }
92}
93
94impl DebugAdapterState {
95 fn new() -> Self {
96 Self {
97 log_messages: VecDeque::new(),
98 rpc_messages: RpcMessages::new(),
99 }
100 }
101}
102
103impl 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
343pub struct DapLogToolbarItemView {
344 log_view: Option<Entity<DapLogView>>,
345}
346
347impl DapLogToolbarItemView {
348 pub fn new() -> Self {
349 Self { log_view: None }
350 }
351}
352
353impl 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
468impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
469
470impl 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
491impl 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
655fn 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)]
667pub(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
674const ADAPTER_LOGS: &str = "Adapter Logs";
675const RPC_MESSAGES: &str = "RPC Messages";
676
677impl 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
685actions!(debug, [OpenDebuggerAdapterLogs]);
686
687pub 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
721impl 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
741impl 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
828impl Focusable for DapLogView {
829 fn focus_handle(&self, _cx: &App) -> FocusHandle {
830 self.focus_handle.clone()
831 }
832}
833
834pub enum Event {
835 NewLogEntry {
836 id: SessionId,
837 entry: String,
838 kind: LogKind,
839 },
840}
841
842impl EventEmitter<Event> for LogStore {}
843impl EventEmitter<Event> for DapLogView {}
844impl EventEmitter<EditorEvent> for DapLogView {}
845impl EventEmitter<SearchEvent> for DapLogView {}
crates/debugger_tools/src/debugger_tools.rs+8−0
@@ -0,0 +1,8 @@
1mod dap_log;
2pub use dap_log::*;
3
4use gpui::App;
5
6pub fn init(cx: &mut App) {
7 dap_log::init(cx);
8}
crates/debugger_ui/Cargo.toml+58−0
@@ -0,0 +1,58 @@
1[package]
2name = "debugger_ui"
3version = "0.1.0"
4edition.workspace = true
5publish.workspace = true
6license = "GPL-3.0-or-later"
7
8[lints]
9workspace = true
10
11[features]
12test-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]
22anyhow.workspace = true
23client.workspace = true
24collections.workspace = true
25command_palette_hooks.workspace = true
26dap.workspace = true
27editor.workspace = true
28feature_flags.workspace = true
29futures.workspace = true
30fuzzy.workspace = true
31gpui.workspace = true
32language.workspace = true
33log.workspace = true
34menu.workspace = true
35picker.workspace = true
36pretty_assertions.workspace = true
37project.workspace = true
38rpc.workspace = true
39serde.workspace = true
40serde_json.workspace = true
41settings.workspace = true
42sysinfo.workspace = true
43task.workspace = true
44terminal_view.workspace = true
45theme.workspace = true
46ui.workspace = true
47util.workspace = true
48workspace.workspace = true
49
50[dev-dependencies]
51dap = { workspace = true, features = ["test-support"] }
52editor = { workspace = true, features = ["test-support"] }
53env_logger.workspace = true
54gpui = { workspace = true, features = ["test-support"] }
55project = { workspace = true, features = ["test-support"] }
56unindent.workspace = true
57util = { workspace = true, features = ["test-support"] }
58workspace = { 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 @@
1use dap::DebugRequestType;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::Subscription;
4use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
5use picker::{Picker, PickerDelegate};
6use project::debugger::attach_processes;
7
8use std::sync::Arc;
9use sysinfo::System;
10use ui::{prelude::*, Context, Tooltip};
11use ui::{ListItem, ListItemSpacing};
12use util::debug_panic;
13use workspace::ModalView;
14
15#[derive(Debug, Clone)]
16struct Candidate {
17 pid: u32,
18 name: String,
19 command: Vec<String>,
20}
21
22pub(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
31impl 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
44pub struct AttachModal {
45 _subscription: Subscription,
46 pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
47}
48
49impl 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
68impl 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
77impl EventEmitter<DismissEvent> for AttachModal {}
78
79impl Focusable for AttachModal {
80 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
81 self.picker.read(cx).focus_handle(cx)
82 }
83}
84
85impl ModalView for AttachModal {}
86
87impl 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"))]
284pub(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 @@
1use crate::session::DebugSession;
2use anyhow::{anyhow, Result};
3use collections::HashMap;
4use command_palette_hooks::CommandPaletteFilter;
5use dap::{
6 client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
7 ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
8};
9use futures::{channel::mpsc, SinkExt as _};
10use gpui::{
11 actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
12 Focusable, Subscription, Task, WeakEntity,
13};
14use project::{
15 debugger::dap_store::{self, DapStore},
16 terminals::TerminalKind,
17 Project,
18};
19use rpc::proto::{self};
20use settings::Settings;
21use std::{any::TypeId, path::PathBuf};
22use terminal_view::terminal_panel::TerminalPanel;
23use ui::prelude::*;
24use workspace::{
25 dock::{DockPosition, Panel, PanelEvent},
26 pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
27 ToggleIgnoreBreakpoints, Workspace,
28};
29
30pub 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
47actions!(debug_panel, [ToggleFocus]);
48pub struct DebugPanel {
49 size: Pixels,
50 pane: Entity<Pane>,
51 project: WeakEntity<Project>,
52 workspace: WeakEntity<Workspace>,
53 _subscriptions: Vec<Subscription>,
54}
55
56impl 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
436impl EventEmitter<PanelEvent> for DebugPanel {}
437impl EventEmitter<DebugPanelEvent> for DebugPanel {}
438impl EventEmitter<project::Event> for DebugPanel {}
439
440impl Focusable for DebugPanel {
441 fn focus_handle(&self, cx: &App) -> FocusHandle {
442 self.pane.focus_handle(cx)
443 }
444}
445
446impl 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
527impl 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 @@
1use dap::debugger_settings::DebuggerSettings;
2use debugger_panel::{DebugPanel, ToggleFocus};
3use feature_flags::{Debugger, FeatureFlagViewExt};
4use gpui::App;
5use session::DebugSession;
6use settings::Settings;
7use workspace::{
8 Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
9 ToggleIgnoreBreakpoints, Workspace,
10};
11
12pub mod attach_modal;
13pub mod debugger_panel;
14pub mod session;
15
16#[cfg(test)]
17mod tests;
18
19pub 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 @@
1mod failed;
2mod inert;
3pub mod running;
4mod starting;
5
6use std::time::Duration;
7
8use dap::client::SessionId;
9use failed::FailedState;
10use gpui::{
11 percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
12 Focusable, Subscription, Task, Transformation, WeakEntity,
13};
14use inert::{InertEvent, InertState};
15use project::debugger::{dap_store::DapStore, session::Session};
16use project::worktree_store::WorktreeStore;
17use project::Project;
18use rpc::proto::{self, PeerId};
19use running::RunningState;
20use starting::{StartingEvent, StartingState};
21use ui::prelude::*;
22use workspace::{
23 item::{self, Item},
24 FollowableItem, ViewId, Workspace,
25};
26
27pub(crate) enum DebugSessionState {
28 Inert(Entity<InertState>),
29 Starting(Entity<StartingState>),
30 Failed(Entity<FailedState>),
31 Running(Entity<running::RunningState>),
32}
33
34impl 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
43pub 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)]
53pub enum DebugPanelItemEvent {
54 Close,
55 Stopped { go_to_stack_frame: bool },
56}
57
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
59pub enum ThreadItem {
60 Console,
61 LoadedSource,
62 Modules,
63 Variables,
64}
65
66impl 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}
185impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
186
187impl 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
198impl 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
230impl 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
296impl 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 @@
1use gpui::{FocusHandle, Focusable};
2use ui::{
3 h_flex, Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window,
4};
5
6pub(crate) struct FailedState {
7 focus_handle: FocusHandle,
8}
9impl FailedState {
10 pub(super) fn new(cx: &mut Context<Self>) -> Self {
11 Self {
12 focus_handle: cx.focus_handle(),
13 }
14 }
15}
16
17impl Focusable for FailedState {
18 fn focus_handle(&self, _: &ui::App) -> FocusHandle {
19 self.focus_handle.clone()
20 }
21}
22impl 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 @@
1use std::path::PathBuf;
2
3use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
4use editor::{Editor, EditorElement, EditorStyle};
5use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
6use settings::Settings as _;
7use task::TCPHost;
8use theme::ThemeSettings;
9use 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};
14use workspace::Workspace;
15
16use crate::attach_modal::AttachModal;
17
18pub(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
26impl 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}
53impl Focusable for InertState {
54 fn focus_handle(&self, _cx: &App) -> FocusHandle {
55 self.focus_handle.clone()
56 }
57}
58
59pub(crate) enum InertEvent {
60 Spawned { config: DebugAdapterConfig },
61}
62
63impl EventEmitter<InertEvent> for InertState {}
64
65static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
66
67impl 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
155fn 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}
167impl 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 @@
1mod console;
2mod loaded_source_list;
3mod module_list;
4pub mod stack_frame_list;
5pub mod variable_list;
6
7use super::{DebugPanelItemEvent, ThreadItem};
8use console::Console;
9use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread};
10use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
11use loaded_source_list::LoadedSourceList;
12use module_list::ModuleList;
13use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
14use rpc::proto::ViewId;
15use settings::Settings;
16use stack_frame_list::StackFrameList;
17use 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};
23use util::ResultExt;
24use variable_list::VariableList;
25use workspace::Workspace;
26
27pub 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
44impl 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
353impl 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
680impl EventEmitter<DebugPanelItemEvent> for RunningState {}
681
682impl 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 @@
1use super::{
2 stack_frame_list::{StackFrameList, StackFrameListEvent},
3 variable_list::VariableList,
4};
5use anyhow::Result;
6use collections::HashMap;
7use dap::OutputEvent;
8use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
9use fuzzy::StringMatchCandidate;
10use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
11use language::{Buffer, CodeLabel};
12use menu::Confirm;
13use project::{
14 debugger::session::{CompletionsQuery, OutputToken, Session},
15 Completion,
16};
17use settings::Settings;
18use std::{cell::RefCell, rc::Rc, usize};
19use theme::ThemeSettings;
20use ui::prelude::*;
21
22pub 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
33impl 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
212impl 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
244struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
245
246impl 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
307impl 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 @@
1use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
2use project::debugger::session::{Session, SessionEvent};
3use ui::prelude::*;
4use util::maybe;
5
6pub struct LoadedSourceList {
7 list: ListState,
8 invalidate: bool,
9 focus_handle: FocusHandle,
10 _subscription: Subscription,
11 session: Entity<Session>,
12}
13
14impl 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
80impl Focusable for LoadedSourceList {
81 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
82 self.focus_handle.clone()
83 }
84}
85
86impl 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 @@
1use anyhow::anyhow;
2use gpui::{
3 list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, WeakEntity,
4};
5use project::{
6 debugger::session::{Session, SessionEvent},
7 ProjectItem as _, ProjectPath,
8};
9use std::{path::Path, sync::Arc};
10use ui::prelude::*;
11use util::maybe;
12use workspace::Workspace;
13
14pub 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
23impl 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"))]
153impl 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
160impl Focusable for ModuleList {
161 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
162 self.focus_handle.clone()
163 }
164}
165
166impl 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 @@
1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Result};
5use dap::StackFrameId;
6use gpui::{
7 list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
8 WeakEntity,
9};
10
11use language::PointUtf16;
12use project::debugger::session::{Session, SessionEvent, StackFrame};
13use project::{ProjectItem, ProjectPath};
14use ui::{prelude::*, Tooltip};
15use util::ResultExt;
16use workspace::Workspace;
17
18use super::RunningState;
19
20#[derive(Debug)]
21pub enum StackFrameListEvent {
22 SelectedStackFrameChanged(StackFrameId),
23}
24
25pub 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)]
39pub enum StackFrameEntry {
40 Normal(dap::StackFrame),
41 Collapsed(Vec<dap::StackFrame>),
42}
43
44impl 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
498impl 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
513impl Focusable for StackFrameList {
514 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
515 self.focus_handle.clone()
516 }
517}
518
519impl EventEmitter<StackFrameListEvent> for StackFrameList {}
crates/debugger_ui/src/session/running/variable_list.rs+946−0
@@ -0,0 +1,946 @@
1use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
2use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
3use editor::Editor;
4use 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};
9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
10use project::debugger::session::{Session, SessionEvent};
11use std::{collections::HashMap, ops::Range, sync::Arc};
12use ui::{prelude::*, ContextMenu, ListItem, Scrollbar, ScrollbarState};
13use util::{debug_panic, maybe};
14
15actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18pub(crate) struct EntryState {
19 depth: usize,
20 is_expanded: bool,
21 parent_reference: VariableReference,
22}
23
24#[derive(Debug, PartialEq, Eq, Hash, Clone)]
25pub(crate) struct EntryPath {
26 pub leaf_name: Option<SharedString>,
27 pub indices: Arc<[SharedString]>,
28}
29
30impl 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)]
60enum EntryKind {
61 Variable(dap::Variable),
62 Scope(dap::Scope),
63}
64
65impl 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)]
90struct ListEntry {
91 dap_kind: EntryKind,
92 path: EntryPath,
93}
94
95impl 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
130pub 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
145impl 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
881impl Focusable for VariableList {
882 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
883 self.focus_handle.clone()
884 }
885}
886
887impl 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
932struct EntryColors {
933 default: Hsla,
934 hover: Hsla,
935 marked_active: Hsla,
936}
937
938fn 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 @@
1use std::time::Duration;
2
3use anyhow::Result;
4
5use dap::client::SessionId;
6use gpui::{
7 percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task,
8 Transformation,
9};
10use project::debugger::session::Session;
11use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled};
12
13pub(crate) struct StartingState {
14 focus_handle: FocusHandle,
15 pub(super) session_id: SessionId,
16 _notify_parent: Task<()>,
17}
18
19pub(crate) enum StartingEvent {
20 Failed,
21 Finished(Entity<Session>),
22}
23
24impl EventEmitter<StartingEvent> for StartingState {}
25
26impl 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
52impl Focusable for StartingState {
53 fn focus_handle(&self, _: &ui::App) -> FocusHandle {
54 self.focus_handle.clone()
55 }
56}
57
58impl 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 @@
1use gpui::{Entity, TestAppContext, WindowHandle};
2use project::Project;
3use settings::SettingsStore;
4use terminal_view::terminal_panel::TerminalPanel;
5use workspace::Workspace;
6
7use crate::{debugger_panel::DebugPanel, session::DebugSession};
8
9mod attach_modal;
10mod console;
11mod debugger_panel;
12mod module_list;
13mod stack_frame_list;
14mod variable_list;
15
16pub 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
35pub 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
63pub 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 @@
1use crate::*;
2use attach_modal::AttachModal;
3use dap::client::SessionId;
4use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
5use menu::Confirm;
6use project::{FakeFs, Project};
7use serde_json::json;
8use task::AttachConfig;
9use tests::{init_test, init_test_workspace};
10
11#[gpui::test]
12async 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]
63async 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 @@
1use crate::{tests::active_debug_session_panel, *};
2use dap::requests::StackTrace;
3use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
4use project::{FakeFs, Project};
5use serde_json::json;
6use tests::{init_test, init_test_workspace};
7
8#[gpui::test]
9async 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 @@
1use crate::*;
2use 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};
11use editor::{
12 actions::{self},
13 Editor, EditorMode, MultiBuffer,
14};
15use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
16use project::{
17 debugger::session::{ThreadId, ThreadStatus},
18 FakeFs, Project,
19};
20use serde_json::json;
21use std::{
22 path::Path,
23 sync::{
24 atomic::{AtomicBool, Ordering},
25 Arc,
26 },
27};
28use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
29use tests::{active_debug_session_panel, init_test, init_test_workspace};
30use util::path;
31use workspace::{dock::Panel, Item};
32
33#[gpui::test]
34async 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]
183async 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]
365async 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]
455async 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]
535async 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]
649async 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]
871async 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]
1044async 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 @@
1use crate::{
2 debugger_panel::DebugPanel,
3 session::ThreadItem,
4 tests::{active_debug_session_panel, init_test, init_test_workspace},
5};
6use dap::{
7 requests::{Modules, StackTrace, Threads},
8 DebugRequestType, StoppedEvent,
9};
10use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
11use project::{FakeFs, Project};
12use std::sync::{
13 atomic::{AtomicBool, AtomicI32, Ordering},
14 Arc,
15};
16
17#[gpui::test]
18async 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 @@
1use crate::{
2 debugger_panel::DebugPanel,
3 session::running::stack_frame_list::StackFrameEntry,
4 tests::{active_debug_session_panel, init_test, init_test_workspace},
5};
6use dap::{
7 requests::{StackTrace, Threads},
8 StackFrame,
9};
10use editor::{Editor, ToPoint as _};
11use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
12use project::{FakeFs, Project};
13use serde_json::json;
14use std::sync::Arc;
15use unindent::Unindent as _;
16use util::path;
17
18#[gpui::test]
19async 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]
206async 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]
483async 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 @@
1use std::sync::{
2 atomic::{AtomicBool, Ordering},
3 Arc,
4};
5
6use crate::{
7 session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
8 tests::{active_debug_session_panel, init_test, init_test_workspace},
9 DebugPanel,
10};
11use collections::HashMap;
12use dap::{
13 requests::{Initialize, Launch, Scopes, StackTrace, Variables},
14 Scope, StackFrame, Variable,
15};
16use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
17use menu::{SelectFirst, SelectNext, SelectPrevious};
18use project::{FakeFs, Project};
19use serde_json::json;
20use unindent::Unindent as _;
21use util::path;
22
23/// This only tests fetching one scope and 2 variables for a single stackframe
24#[gpui::test]
25async 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]
248async 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]
530async 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]
1116async 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]
1388async 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
40db.workspace = true40db.workspace = true
41buffer_diff.workspace = true41buffer_diff.workspace = true
42emojis.workspace = true42emojis.workspace = true
43feature_flags.workspace = true
43file_icons.workspace = true44file_icons.workspace = true
44futures.workspace = true45futures.workspace = true
45fuzzy.workspace = true46fuzzy.workspace = true
@@ -55,6 +56,7 @@ linkify.workspace = true · 9 unmodified lines
55log.workspace = true56log.workspace = true
56lsp.workspace = true57lsp.workspace = true
57markdown.workspace = true58markdown.workspace = true
59menu.workspace = true
58multi_buffer.workspace = true60multi_buffer.workspace = true
59ordered-float.workspace = true61ordered-float.workspace = true
60parking_lot.workspace = true62parking_lot.workspace = true
crates/editor/src/actions.rs+2−0
@@ -412,6 +412,8 @@ actions!( · 411 unmodified lines
412 SwitchSourceHeader,412 SwitchSourceHeader,
413 Tab,413 Tab,
414 Backtab,414 Backtab,
415 ToggleBreakpoint,
416 EditLogBreakpoint,
415 ToggleAutoSignatureHelp,417 ToggleAutoSignatureHelp,
416 ToggleGitBlameInline,418 ToggleGitBlameInline,
417 ToggleIndentGuides,419 ToggleIndentGuides,
crates/editor/src/editor.rs+761−7
@@ -68,6 +68,7 @@ use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, Posi · 67 unmodified lines
68pub use element::{68pub use element::{
69 CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,69 CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
70};70};
71use feature_flags::{Debugger, FeatureFlagAppExt};
71use futures::{72use futures::{
72 future::{self, join, Shared},73 future::{self, join, Shared},
73 FutureExt,74 FutureExt,
@@ -83,7 +84,7 @@ use git::blame::GitBlame; · 9 unmodified lines
83use gpui::{84use gpui::{
84 div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,85 div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
85 AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background,86 AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background,
86 Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,87 Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
87 EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,88 EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,
88 Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,89 Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
89 ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,90 ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
@@ -91,6 +92,7 @@ use gpui::{ · 1 unmodified line
91 WeakEntity, WeakFocusHandle, Window,92 WeakEntity, WeakFocusHandle, Window,
92};93};
93use highlight_matching_bracket::refresh_matching_bracket_highlights;94use highlight_matching_bracket::refresh_matching_bracket_highlights;
95use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
94use hover_popover::{hide_hover, HoverState};96use hover_popover::{hide_hover, HoverState};
95use indent_guides::ActiveIndentGuidesState;97use indent_guides::ActiveIndentGuidesState;
96use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};98use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
@@ -112,6 +114,11 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} · 15 unmodified lines
112use linked_editing_ranges::refresh_linked_ranges;114use linked_editing_ranges::refresh_linked_ranges;
113use mouse_context_menu::MouseContextMenu;115use mouse_context_menu::MouseContextMenu;
114use persistence::DB;116use persistence::DB;
117use project::{
118 debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent},
119 ProjectPath,
120};
121
115pub use proposed_changes_editor::{122pub use proposed_changes_editor::{
116 ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,123 ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
117};124};
@@ -119,7 +126,6 @@ use smallvec::smallvec; · 1 unmodified line
119use std::iter::Peekable;126use std::iter::Peekable;
120use task::{ResolvedTask, TaskTemplate, TaskVariables};127use task::{ResolvedTask, TaskTemplate, TaskVariables};
121 128
122use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
123pub use lsp::CompletionContext;129pub use lsp::CompletionContext;
124use lsp::{130use lsp::{
125 CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,131 CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
@@ -136,7 +142,9 @@ use multi_buffer::{ · 10 unmodified lines
136 ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,142 ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
137 MultiOrSingleBufferOffsetRange, ToOffsetUtf16,143 MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
138};144};
145use parking_lot::Mutex;
139use project::{146use project::{
147 debugger::breakpoint_store::{Breakpoint, BreakpointKind},
140 lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},148 lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
141 project_settings::{GitGutterSetting, ProjectSettings},149 project_settings::{GitGutterSetting, ProjectSettings},
142 CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,150 CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
@@ -153,6 +161,7 @@ use serde::{Deserialize, Serialize}; · 10 unmodified lines
153use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};161use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
154use smallvec::SmallVec;162use smallvec::SmallVec;
155use snippet::Snippet;163use snippet::Snippet;
164use std::sync::Arc;
156use std::{165use std::{
157 any::TypeId,166 any::TypeId,
158 borrow::Cow,167 borrow::Cow,
@@ -163,7 +172,6 @@ use std::{ · 4 unmodified lines
163 ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},172 ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
164 path::{Path, PathBuf},173 path::{Path, PathBuf},
165 rc::Rc,174 rc::Rc,
166 sync::Arc,
167 time::{Duration, Instant},175 time::{Duration, Instant},
168};176};
169pub use sum_tree::Bias;177pub use sum_tree::Bias;
@@ -236,6 +244,7 @@ impl InlayId { · 66 unmodified lines
236 }244 }
237}245}
238 246
247pub enum DebugCurrentRowHighlight {}
239enum DocumentHighlightRead {}248enum DocumentHighlightRead {}
240enum DocumentHighlightWrite {}249enum DocumentHighlightWrite {}
241enum InputComposition {}250enum InputComposition {}
@@ -589,6 +598,7 @@ struct ResolvedTasks { · 347 unmodified lines
589 templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,598 templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
590 position: Anchor,599 position: Anchor,
591}600}
601
592#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]602#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
593struct BufferOffset(usize);603struct BufferOffset(usize);
594 604
@@ -659,6 +669,7 @@ pub struct Editor { · 64 unmodified lines
659 show_git_diff_gutter: Option<bool>,669 show_git_diff_gutter: Option<bool>,
660 show_code_actions: Option<bool>,670 show_code_actions: Option<bool>,
661 show_runnables: Option<bool>,671 show_runnables: Option<bool>,
672 show_breakpoints: Option<bool>,
662 show_wrap_guides: Option<bool>,673 show_wrap_guides: Option<bool>,
663 show_indent_guides: Option<bool>,674 show_indent_guides: Option<bool>,
664 placeholder_text: Option<Arc<str>>,675 placeholder_text: Option<Arc<str>>,
@@ -749,6 +760,11 @@ pub struct Editor { · 84 unmodified lines
749 expect_bounds_change: Option<Bounds<Pixels>>,760 expect_bounds_change: Option<Bounds<Pixels>>,
750 tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,761 tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
751 tasks_update_task: Option<Task<()>>,762 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>,
752 in_project_search: bool,768 in_project_search: bool,
753 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,769 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
754 breadcrumb_header: Option<String>,770 breadcrumb_header: Option<String>,
@@ -789,6 +805,7 @@ pub struct EditorSnapshot { · 34 unmodified lines
789 show_git_diff_gutter: Option<bool>,805 show_git_diff_gutter: Option<bool>,
790 show_code_actions: Option<bool>,806 show_code_actions: Option<bool>,
791 show_runnables: Option<bool>,807 show_runnables: Option<bool>,
808 show_breakpoints: Option<bool>,
792 git_blame_gutter_max_author_length: Option<usize>,809 git_blame_gutter_max_author_length: Option<usize>,
793 pub display_snapshot: DisplaySnapshot,810 pub display_snapshot: DisplaySnapshot,
794 pub placeholder_text: Option<Arc<str>>,811 pub placeholder_text: Option<Arc<str>>,
@@ -1279,7 +1296,18 @@ impl Editor { · 484 unmodified lines
1279 editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));1296 editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
1280 },1297 },
1281 ));1298 ));
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 ));
1283 }1311 }
1284 }1312 }
1285 1313
@@ -1303,6 +1331,11 @@ impl Editor { · 17 unmodified lines
1303 None1331 None
1304 };1332 };
1305 1333
1334 let breakpoint_store = match (mode, project.as_ref()) {
1335 (EditorMode::Full, Some(project)) => Some(project.read(cx).breakpoint_store()),
1336 _ => None,
1337 };
1338
1306 let mut code_action_providers = Vec::new();1339 let mut code_action_providers = Vec::new();
1307 let mut load_uncommitted_diff = None;1340 let mut load_uncommitted_diff = None;
1308 if let Some(project) = project.clone() {1341 if let Some(project) = project.clone() {
@@ -1356,6 +1389,7 @@ impl Editor { · 47 unmodified lines
1356 show_git_diff_gutter: None,1389 show_git_diff_gutter: None,
1357 show_code_actions: None,1390 show_code_actions: None,
1358 show_runnables: None,1391 show_runnables: None,
1392 show_breakpoints: None,
1359 show_wrap_guides: None,1393 show_wrap_guides: None,
1360 show_indent_guides,1394 show_indent_guides,
1361 placeholder_text: None,1395 placeholder_text: None,
@@ -1440,6 +1474,9 @@ impl Editor { · 78 unmodified lines
1440 blame: None,1474 blame: None,
1441 blame_subscription: None,1475 blame_subscription: None,
1442 tasks: Default::default(),1476 tasks: Default::default(),
1477
1478 breakpoint_store,
1479 gutter_breakpoint_indicator: None,
1443 _subscriptions: vec![1480 _subscriptions: vec![
1444 cx.observe(&buffer, Self::on_buffer_changed),1481 cx.observe(&buffer, Self::on_buffer_changed),
1445 cx.subscribe_in(&buffer, window, Self::on_buffer_event),1482 cx.subscribe_in(&buffer, window, Self::on_buffer_event),
@@ -1474,6 +1511,12 @@ impl Editor { · 28 unmodified lines
1474 text_style_refinement: None,1511 text_style_refinement: None,
1475 load_diff_task: load_uncommitted_diff,1512 load_diff_task: load_uncommitted_diff,
1476 };1513 };
1514 if let Some(breakpoints) = this.breakpoint_store.as_ref() {
1515 this._subscriptions
1516 .push(cx.observe(breakpoints, |_, _, cx| {
1517 cx.notify();
1518 }));
1519 }
1477 this.tasks_update_task = Some(this.refresh_runnables(window, cx));1520 this.tasks_update_task = Some(this.refresh_runnables(window, cx));
1478 this._subscriptions.extend(project_subscriptions);1521 this._subscriptions.extend(project_subscriptions);
1479 1522
@@ -1490,6 +1533,8 @@ impl Editor { · 10 unmodified lines
1490 this.start_git_blame_inline(false, window, cx);1533 this.start_git_blame_inline(false, window, cx);
1491 }1534 }
1492 1535
1536 this.go_to_active_debug_line(window, cx);
1537
1493 if let Some(buffer) = buffer.read(cx).as_singleton() {1538 if let Some(buffer) = buffer.read(cx).as_singleton() {
1494 if let Some(project) = this.project.as_ref() {1539 if let Some(project) = this.project.as_ref() {
1495 let handle = project.update(cx, |project, cx| {1540 let handle = project.update(cx, |project, cx| {
@@ -1764,6 +1809,7 @@ impl Editor { · 268 unmodified lines
1764 show_git_diff_gutter: self.show_git_diff_gutter,1809 show_git_diff_gutter: self.show_git_diff_gutter,
1765 show_code_actions: self.show_code_actions,1810 show_code_actions: self.show_code_actions,
1766 show_runnables: self.show_runnables,1811 show_runnables: self.show_runnables,
1812 show_breakpoints: self.show_breakpoints,
1767 git_blame_gutter_max_author_length,1813 git_blame_gutter_max_author_length,
1768 display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),1814 display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
1769 scroll_anchor: self.scroll_manager.anchor(),1815 scroll_anchor: self.scroll_manager.anchor(),
@@ -5857,14 +5903,28 @@ impl Editor { · 4087 unmodified lines
5857 _style: &EditorStyle,5903 _style: &EditorStyle,
5858 row: DisplayRow,5904 row: DisplayRow,
5859 is_active: bool,5905 is_active: bool,
5906 breakpoint: Option<&(Anchor, Breakpoint)>,
5860 cx: &mut Context<Self>,5907 cx: &mut Context<Self>,
5861 ) -> Option<IconButton> {5908 ) -> 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
5862 if self.available_code_actions.is_some() {5922 if self.available_code_actions.is_some() {
5863 Some(5923 Some(
5864 IconButton::new("code_actions_indicator", ui::IconName::Bolt)5924 IconButton::new("code_actions_indicator", ui::IconName::Bolt)
5865 .shape(ui::IconButtonShape::Square)5925 .shape(ui::IconButtonShape::Square)
5866 .icon_size(IconSize::XSmall)5926 .icon_size(IconSize::XSmall)
5867 .icon_color(Color::Muted)5927 .icon_color(color)
5868 .toggle_state(is_active)5928 .toggle_state(is_active)
5869 .tooltip({5929 .tooltip({
5870 let focus_handle = self.focus_handle.clone();5930 let focus_handle = self.focus_handle.clone();
@@ -5889,6 +5949,16 @@ impl Editor { · 18 unmodified lines
5889 window,5949 window,
5890 cx,5950 cx,
5891 );5951 );
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 );
5892 })),5962 })),
5893 )5963 )
5894 } else {5964 } else {
@@ -5907,6 +5977,198 @@ impl Editor { · 12 unmodified lines
5907 }5977 }
5908 }5978 }
5909 5979
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
5910 fn build_tasks_context(6172 fn build_tasks_context(
5911 project: &Entity<Project>,6173 project: &Entity<Project>,
5912 buffer: &Entity<Buffer>,6174 buffer: &Entity<Buffer>,
@@ -6043,12 +6305,26 @@ impl Editor { · 130 unmodified lines
6043 _style: &EditorStyle,6305 _style: &EditorStyle,
6044 is_active: bool,6306 is_active: bool,
6045 row: DisplayRow,6307 row: DisplayRow,
6308 breakpoint: Option<(Anchor, Breakpoint)>,
6046 cx: &mut Context<Self>,6309 cx: &mut Context<Self>,
6047 ) -> IconButton {6310 ) -> 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
6048 IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)6324 IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
6049 .shape(ui::IconButtonShape::Square)6325 .shape(ui::IconButtonShape::Square)
6050 .icon_size(IconSize::XSmall)6326 .icon_size(IconSize::XSmall)
6051 .icon_color(Color::Muted)6327 .icon_color(color)
6052 .toggle_state(is_active)6328 .toggle_state(is_active)
6053 .on_click(cx.listener(move |editor, _e, window, cx| {6329 .on_click(cx.listener(move |editor, _e, window, cx| {
6054 window.focus(&editor.focus_handle(cx));6330 window.focus(&editor.focus_handle(cx));
@@ -6060,6 +6336,16 @@ impl Editor { · 5 unmodified lines
6060 cx,6336 cx,
6061 );6337 );
6062 }))6338 }))
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 }))
6063 }6349 }
6064 6350
6065 pub fn context_menu_visible(&self) -> bool {6351 pub fn context_menu_visible(&self) -> bool {
@@ -8032,6 +8318,235 @@ impl Editor { · 1966 unmodified lines
8032 }8318 }
8033 }8319 }
8034 8320
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
8035 pub fn prepare_restore_change(8550 pub fn prepare_restore_change(
8036 &self,8551 &self,
8037 revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,8552 revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
@@ -11866,6 +12381,33 @@ impl Editor { · 3828 unmodified lines
11866 .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))12381 .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
11867 }12382 }
11868 12383
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
11869 pub fn go_to_definition(12411 pub fn go_to_definition(
11870 &mut self,12412 &mut self,
11871 _: &GoToDefinition,12413 _: &GoToDefinition,
@@ -14532,6 +15074,11 @@ impl Editor { · 2660 unmodified lines
14532 cx.notify();15074 cx.notify();
14533 }15075 }
14534 15076
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
14535 pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {15082 pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
14536 if self.display_map.read(cx).masked != masked {15083 if self.display_map.read(cx).masked != masked {
14537 self.display_map.update(cx, |map, _| map.masked = masked);15084 self.display_map.update(cx, |map, _| map.masked = masked);
@@ -14634,6 +15181,61 @@ impl Editor { · 96 unmodified lines
14634 }15181 }
14635 }15182 }
14636 15183
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
14637 pub fn copy_file_name_without_extension(15239 pub fn copy_file_name_without_extension(
14638 &mut self,15240 &mut self,
14639 _: &CopyFileNameWithoutExtension,15241 _: &CopyFileNameWithoutExtension,
@@ -17645,6 +18247,7 @@ impl EditorSnapshot { · 3005 unmodified lines
17645 .unwrap_or(gutter_settings.code_actions);18247 .unwrap_or(gutter_settings.code_actions);
17646 18248
17647 let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables);18249 let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables);
18250 let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints);
17648 18251
17649 let git_blame_entries_width =18252 let git_blame_entries_width =
17650 self.git_blame_gutter_max_author_length18253 self.git_blame_gutter_max_author_length
@@ -17668,7 +18271,7 @@ impl EditorSnapshot { · 17 unmodified lines
17668 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);18271 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
17669 left_padding += if !is_singleton {18272 left_padding += if !is_singleton {
17670 em_width * 4.018273 em_width * 4.0
17671 } else if show_code_actions || show_runnables {18274 } else if show_code_actions || show_runnables || show_breakpoints {
17672 em_width * 3.018275 em_width * 3.0
17673 } else if show_git_gutter && show_line_numbers {18276 } else if show_git_gutter && show_line_numbers {
17674 em_width * 2.018277 em_width * 2.0
@@ -18668,6 +19271,157 @@ impl Global for KillRing {} · 993 unmodified lines
18668 19271
18669const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);19272const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
18670 19273
19274struct 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
19284impl 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
19402impl 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
19419impl Focusable for BreakpointPromptEditor {
19420 fn focus_handle(&self, cx: &App) -> FocusHandle {
19421 self.prompt.focus_handle(cx)
19422 }
19423}
19424
18671fn all_edits_insertions_or_deletions(19425fn all_edits_insertions_or_deletions(
18672 edits: &Vec<(Range<Anchor>, String)>,19426 edits: &Vec<(Range<Anchor>, String)>,
18673 snapshot: &MultiBufferSnapshot,19427 snapshot: &MultiBufferSnapshot,
crates/editor/src/editor_settings.rs+5−0
@@ -117,6 +117,7 @@ pub struct Gutter { · 116 unmodified lines
117 pub line_numbers: bool,117 pub line_numbers: bool,
118 pub code_actions: bool,118 pub code_actions: bool,
119 pub runnables: bool,119 pub runnables: bool,
120 pub breakpoints: bool,
120 pub folds: bool,121 pub folds: bool,
121}122}
122 123
@@ -464,6 +465,10 @@ pub struct GutterContent { · 341 unmodified lines
464 ///465 ///
465 /// Default: true466 /// Default: true
466 pub runnables: Option<bool>,467 pub runnables: Option<bool>,
468 /// Whether to show breakpoints in the gutter.
469 ///
470 /// Default: true
471 pub breakpoints: Option<bool>,
467 /// Whether to show fold buttons in the gutter.472 /// Whether to show fold buttons in the gutter.
468 ///473 ///
469 /// Default: true474 /// Default: true
crates/editor/src/editor_tests.rs+337−2
@@ -28,8 +28,11 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings}; · 27 unmodified lines
28use multi_buffer::{IndentGuide, PathKey};28use multi_buffer::{IndentGuide, PathKey};
29use parking_lot::Mutex;29use parking_lot::Mutex;
30use pretty_assertions::{assert_eq, assert_ne};30use pretty_assertions::{assert_eq, assert_ne};
31use project::project_settings::{LspSettings, ProjectSettings};31use project::{
32use project::FakeFs;32 debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint},
33 project_settings::{LspSettings, ProjectSettings},
34 FakeFs,
35};
33use serde_json::{self, json};36use serde_json::{self, json};
34use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};37use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
35use std::{38use std::{
@@ -11924,6 +11927,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) { · 11888 unmodified lines
11924 cx.update_editor(|editor, window, cx| {11927 cx.update_editor(|editor, window, cx| {
11925 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)11928 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11926 });11929 });
11930 cx.run_until_parked();
11927 cx.assert_editor_state(after);11931 cx.assert_editor_state(after);
11928 };11932 };
11929 11933
@@ -17111,6 +17115,337 @@ async fn assert_highlighted_edits( · 5181 unmodified lines
17111 });17115 });
17112}17116}
17113 17117
17118#[track_caller]
17119fn 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
17140fn 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]
17171async 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]
17293async 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
17114#[gpui::test]17449#[gpui::test]
17115async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {17450async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17116 init_test(cx, |_| {});17451 init_test(cx, |_| {});
crates/editor/src/element.rs+199−41
@@ -29,6 +29,7 @@ use crate::{ · 28 unmodified lines
29use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};29use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
30use client::ParticipantIndex;30use client::ParticipantIndex;
31use collections::{BTreeMap, HashMap, HashSet};31use collections::{BTreeMap, HashMap, HashSet};
32use feature_flags::{Debugger, FeatureFlagAppExt};
32use file_icons::FileIcons;33use file_icons::FileIcons;
33use git::{blame::BlameEntry, status::FileStatus, Oid};34use git::{blame::BlameEntry, status::FileStatus, Oid};
34use gpui::{35use gpui::{
@@ -55,7 +56,10 @@ use multi_buffer::{ · 20 unmodified lines
55 Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,56 Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,
56 MultiBufferRow, RowInfo,57 MultiBufferRow, RowInfo,
57};58};
58use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};59use project::{
60 debugger::breakpoint_store::{Breakpoint, BreakpointKind},
61 project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
62};
59use settings::Settings;63use settings::Settings;
60use smallvec::{smallvec, SmallVec};64use smallvec::{smallvec, SmallVec};
61use std::{65use std::{
@@ -82,6 +86,14 @@ use workspace::{item::Item, notifications::NotifyTaskExt}; · 20 unmodified lines
82const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;86const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
83const MIN_SCROLL_THUMB_SIZE: f32 = 25.;87const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
84 88
89/// Determines what kinds of highlights should be applied to a lines background.
90#[derive(Clone, Copy, Default)]
91struct LineHighlightSpec {
92 selection: bool,
93 breakpoint: bool,
94 _active_stack_frame: bool,
95}
96
85struct SelectionLayout {97struct SelectionLayout {
86 head: DisplayPoint,98 head: DisplayPoint,
87 cursor_shape: CursorShape,99 cursor_shape: CursorShape,
@@ -509,6 +521,10 @@ impl EditorElement { · 421 unmodified lines
509 register_action(editor, window, Editor::insert_uuid_v4);521 register_action(editor, window, Editor::insert_uuid_v4);
510 register_action(editor, window, Editor::insert_uuid_v7);522 register_action(editor, window, Editor::insert_uuid_v7);
511 register_action(editor, window, Editor::open_selections_in_multibuffer);523 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 }
512 }528 }
513 529
514 fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {530 fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
@@ -875,6 +891,18 @@ impl EditorElement { · 360 unmodified lines
875 let gutter_hovered = gutter_hitbox.is_hovered(window);891 let gutter_hovered = gutter_hitbox.is_hovered(window);
876 editor.set_gutter_hovered(gutter_hovered, cx);892 editor.set_gutter_hovered(gutter_hovered, cx);
877 893
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
878 // Don't trigger hover popover if mouse is hovering over context menu906 // Don't trigger hover popover if mouse is hovering over context menu
879 if text_hitbox.is_hovered(window) {907 if text_hitbox.is_hovered(window) {
880 let point_for_position = position_map.point_for_position(event.position);908 let point_for_position = position_map.point_for_position(event.position);
@@ -970,7 +998,7 @@ impl EditorElement { · 89 unmodified lines
970 cx: &mut App,998 cx: &mut App,
971 ) -> (999 ) -> (
972 Vec<(PlayerColor, Vec<SelectionLayout>)>,1000 Vec<(PlayerColor, Vec<SelectionLayout>)>,
973 BTreeMap<DisplayRow, bool>,1001 BTreeMap<DisplayRow, LineHighlightSpec>,
974 Option<DisplayPoint>,1002 Option<DisplayPoint>,
975 ) {1003 ) {
976 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();1004 let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
@@ -1000,9 +1028,10 @@ impl EditorElement { · 23 unmodified lines
1000 for row in cmp::max(layout.active_rows.start.0, start_row.0)1028 for row in cmp::max(layout.active_rows.start.0, start_row.0)
1001 ..=cmp::min(layout.active_rows.end.0, end_row.0)1029 ..=cmp::min(layout.active_rows.end.0, end_row.0)
1002 {1030 {
1003 let contains_non_empty_selection =1031 let contains_non_empty_selection = active_rows
1004 active_rows.entry(DisplayRow(row)).or_insert(!is_empty);1032 .entry(DisplayRow(row))
1005 *contains_non_empty_selection |= !is_empty;1033 .or_insert_with(LineHighlightSpec::default);
1034 contains_non_empty_selection.selection |= !is_empty;
1006 }1035 }
1007 layouts.push(layout);1036 layouts.push(layout);
1008 }1037 }
@@ -2019,6 +2048,54 @@ impl EditorElement { · 1010 unmodified lines
2019 (offset_y, length)2048 (offset_y, length)
2020 }2049 }
2021 2050
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)]
2022 fn layout_run_indicators(2099 fn layout_run_indicators(
2023 &self,2100 &self,
2024 line_height: Pixels,2101 line_height: Pixels,
@@ -2029,6 +2106,7 @@ impl EditorElement { · 4 unmodified lines
2029 gutter_hitbox: &Hitbox,2106 gutter_hitbox: &Hitbox,
2030 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],2107 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2031 snapshot: &EditorSnapshot,2108 snapshot: &EditorSnapshot,
2109 breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
2032 window: &mut Window,2110 window: &mut Window,
2033 cx: &mut App,2111 cx: &mut App,
2034 ) -> Vec<AnyElement> {2112 ) -> Vec<AnyElement> {
@@ -2088,6 +2166,7 @@ impl EditorElement { · 53 unmodified lines
2088 return None;2166 return None;
2089 }2167 }
2090 }2168 }
2169
2091 let display_row = multibuffer_point.to_display_point(snapshot).row();2170 let display_row = multibuffer_point.to_display_point(snapshot).row();
2092 if row_infos2171 if row_infos
2093 .get((display_row - range.start).0 as usize)2172 .get((display_row - range.start).0 as usize)
@@ -2099,6 +2178,7 @@ impl EditorElement { · 5 unmodified lines
2099 &self.style,2178 &self.style,
2100 Some(display_row) == active_task_indicator_row,2179 Some(display_row) == active_task_indicator_row,
2101 display_row,2180 display_row,
2181 breakpoints.remove(&display_row),
2102 cx,2182 cx,
2103 );2183 );
2104 2184
@@ -2211,6 +2291,7 @@ impl EditorElement { · 106 unmodified lines
2211 scroll_pixel_position: gpui::Point<Pixels>,2291 scroll_pixel_position: gpui::Point<Pixels>,
2212 gutter_dimensions: &GutterDimensions,2292 gutter_dimensions: &GutterDimensions,
2213 gutter_hitbox: &Hitbox,2293 gutter_hitbox: &Hitbox,
2294 breakpoint_points: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
2214 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],2295 display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
2215 window: &mut Window,2296 window: &mut Window,
2216 cx: &mut App,2297 cx: &mut App,
@@ -2226,11 +2307,16 @@ impl EditorElement { · 9 unmodified lines
2226 {2307 {
2227 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);2308 active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
2228 };2309 };
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);
2230 });2313 });
2231 2314
2315 let button = button?;
2316 breakpoint_points.remove(&row);
2317
2232 let button = prepaint_gutter_button(2318 let button = prepaint_gutter_button(
2233 button?,2319 button,
2234 row,2320 row,
2235 line_height,2321 line_height,
2236 gutter_dimensions,2322 gutter_dimensions,
@@ -2310,6 +2396,7 @@ impl EditorElement { · 73 unmodified lines
2310 scroll_position: gpui::Point<f32>,2396 scroll_position: gpui::Point<f32>,
2311 rows: Range<DisplayRow>,2397 rows: Range<DisplayRow>,
2312 buffer_rows: &[RowInfo],2398 buffer_rows: &[RowInfo],
2399 active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
2313 newest_selection_head: Option<DisplayPoint>,2400 newest_selection_head: Option<DisplayPoint>,
2314 snapshot: &EditorSnapshot,2401 snapshot: &EditorSnapshot,
2315 window: &mut Window,2402 window: &mut Window,
@@ -2365,7 +2452,18 @@ impl EditorElement { · 49 unmodified lines
2365 return None;2452 return None;
2366 }2453 }
2367 2454
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);
2369 let shaped_line = self2467 let shaped_line = self
2370 .shape_line_number(SharedString::from(&line_number), color, window)2468 .shape_line_number(SharedString::from(&line_number), color, window)
2371 .log_err()?;2469 .log_err()?;
@@ -2396,7 +2494,6 @@ impl EditorElement { · 24 unmodified lines
2396 let line_number = LineNumberLayout {2494 let line_number = LineNumberLayout {
2397 shaped_line,2495 shaped_line,
2398 hitbox,2496 hitbox,
2399 display_row,
2400 };2497 };
2401 Some((multi_buffer_row, line_number))2498 Some((multi_buffer_row, line_number))
2402 })2499 })
@@ -2408,7 +2505,7 @@ impl EditorElement { · 5 unmodified lines
2408 &self,2505 &self,
2409 rows: Range<DisplayRow>,2506 rows: Range<DisplayRow>,
2410 row_infos: &[RowInfo],2507 row_infos: &[RowInfo],
2411 active_rows: &BTreeMap<DisplayRow, bool>,2508 active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
2412 snapshot: &EditorSnapshot,2509 snapshot: &EditorSnapshot,
2413 window: &mut Window,2510 window: &mut Window,
2414 cx: &mut App,2511 cx: &mut App,
@@ -4035,14 +4132,14 @@ impl EditorElement { · 1620 unmodified lines
4035 .peek()4132 .peek()
4036 .map_or(false, |(active_row, has_selection)| {4133 .map_or(false, |(active_row, has_selection)| {
4037 active_row.0 == end_row + 14134 active_row.0 == end_row + 1
4038 && *has_selection == contains_non_empty_selection4135 && has_selection.selection == contains_non_empty_selection.selection
4039 })4136 })
4040 {4137 {
4041 active_rows.next().unwrap();4138 active_rows.next().unwrap();
4042 end_row += 1;4139 end_row += 1;
4043 }4140 }
4044 4141
4045 if !contains_non_empty_selection {4142 if !contains_non_empty_selection.selection {
4046 let highlight_h_range =4143 let highlight_h_range =
4047 match layout.position_map.snapshot.current_line_highlight {4144 match layout.position_map.snapshot.current_line_highlight {
4048 CurrentLineHighlight::Gutter => Some(Range {4145 CurrentLineHighlight::Gutter => Some(Range {
@@ -4285,32 +4382,31 @@ impl EditorElement { · 236 unmodified lines
4285 for LineNumberLayout {4382 for LineNumberLayout {
4286 shaped_line,4383 shaped_line,
4287 hitbox,4384 hitbox,
4288 display_row,
4289 } in layout.line_numbers.values()4385 } in layout.line_numbers.values()
4290 {4386 {
4291 let Some(hitbox) = hitbox else {4387 let Some(hitbox) = hitbox else {
4292 continue;4388 continue;
4293 };4389 };
4294 4390
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;
4296 4393
4297 let color = if is_active {4394 let Some(line) = self
4298 cx.theme().colors().editor_active_line_number4395 .shape_line_number(shaped_line.text.clone(), color, window)
4299 } else if !is_singleton && hitbox.is_hovered(window) {4396 .log_err()
4300 cx.theme().colors().editor_hover_line_number4397 else {
4301 } else {4398 continue;
4302 cx.theme().colors().editor_line_number4399 };
4303 };
4304 4400
4305 let Some(line) = self4401 line.paint(hitbox.origin, line_height, window, cx).log_err()
4306 .shape_line_number(shaped_line.text.clone(), color, window)4402 } else {
4307 .log_err()4403 shaped_line
4308 else {4404 .paint(hitbox.origin, line_height, window, cx)
4309 continue;4405 .log_err()
4310 };4406 }) else {
4311 let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else {
4312 continue;4407 continue;
4313 };4408 };
4409
4314 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.4410 // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
4315 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.4411 // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
4316 if is_singleton {4412 if is_singleton {
@@ -4335,7 +4431,7 @@ impl EditorElement { · 18 unmodified lines
4335 &layout.position_map.snapshot,4431 &layout.position_map.snapshot,
4336 line_height,4432 line_height,
4337 layout.gutter_hitbox.bounds,4433 layout.gutter_hitbox.bounds,
4338 hunk,4434 &hunk,
4339 );4435 );
4340 Some((4436 Some((
4341 hunk_bounds,4437 hunk_bounds,
@@ -4507,6 +4603,10 @@ impl EditorElement { · 165 unmodified lines
4507 }4603 }
4508 });4604 });
4509 4605
4606 for breakpoint in layout.breakpoints.iter_mut() {
4607 breakpoint.paint(window, cx);
4608 }
4609
4510 for test_indicator in layout.test_indicators.iter_mut() {4610 for test_indicator in layout.test_indicators.iter_mut() {
4511 test_indicator.paint(window, cx);4611 test_indicator.paint(window, cx);
4512 }4612 }
@@ -5707,6 +5807,7 @@ fn prepaint_gutter_button( · 1194 unmodified lines
5707 cx: &mut App,5807 cx: &mut App,
5708) -> AnyElement {5808) -> AnyElement {
5709 let mut button = button.into_any_element();5809 let mut button = button.into_any_element();
5810
5710 let available_space = size(5811 let available_space = size(
5711 AvailableSpace::MinContent,5812 AvailableSpace::MinContent,
5712 AvailableSpace::Definite(line_height),5813 AvailableSpace::Definite(line_height),
@@ -6871,16 +6972,25 @@ impl Element for EditorElement { · 1158 unmodified lines
6871 (selections, selected_buffer_ids)6972 (selections, selected_buffer_ids)
6872 });6973 });
6873 6974
6874 let (selections, active_rows, newest_selection_head) = self.layout_selections(6975 let (selections, mut active_rows, newest_selection_head) = self
6875 start_anchor,6976 .layout_selections(
6876 end_anchor,6977 start_anchor,
6877 &local_selections,6978 end_anchor,
6878 &snapshot,6979 &local_selections,
6879 start_row,6980 &snapshot,
6880 end_row,6981 start_row,
6881 window,6982 end_row,
6882 cx,6983 window,
6883 );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 }
6884 6994
6885 let line_numbers = self.layout_line_numbers(6995 let line_numbers = self.layout_line_numbers(
6886 Some(&gutter_hitbox),6996 Some(&gutter_hitbox),
@@ -6889,12 +6999,36 @@ impl Element for EditorElement { · 2 unmodified lines
6889 scroll_position,6999 scroll_position,
6890 start_row..end_row,7000 start_row..end_row,
6891 &row_infos,7001 &row_infos,
7002 &active_rows,
6892 newest_selection_head,7003 newest_selection_head,
6893 &snapshot,7004 &snapshot,
6894 window,7005 window,
6895 cx,7006 cx,
6896 );7007 );
6897 7008
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
6898 let mut expand_toggles =7032 let mut expand_toggles =
6899 window.with_element_namespace("expand_toggles", |window| {7033 window.with_element_namespace("expand_toggles", |window| {
6900 self.layout_expand_toggles(7034 self.layout_expand_toggles(
@@ -7339,6 +7473,7 @@ impl Element for EditorElement { · 438 unmodified lines
7339 scroll_pixel_position,7473 scroll_pixel_position,
7340 &gutter_dimensions,7474 &gutter_dimensions,
7341 &gutter_hitbox,7475 &gutter_hitbox,
7476 &mut breakpoint_rows,
7342 &display_hunks,7477 &display_hunks,
7343 window,7478 window,
7344 cx,7479 cx,
@@ -7370,6 +7505,7 @@ impl Element for EditorElement { · 25 unmodified lines
7370 &gutter_hitbox,7505 &gutter_hitbox,
7371 &display_hunks,7506 &display_hunks,
7372 &snapshot,7507 &snapshot,
7508 &mut breakpoint_rows,
7373 window,7509 window,
7374 cx,7510 cx,
7375 )7511 )
@@ -7377,6 +7513,26 @@ impl Element for EditorElement { · 1 unmodified line
7377 Vec::new()7513 Vec::new()
7378 };7514 };
7379 7515
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
7380 self.layout_signature_help(7536 self.layout_signature_help(
7381 &hitbox,7537 &hitbox,
7382 content_origin,7538 content_origin,
@@ -7526,6 +7682,7 @@ impl Element for EditorElement { · 143 unmodified lines
7526 diff_hunk_controls,7682 diff_hunk_controls,
7527 mouse_context_menu,7683 mouse_context_menu,
7528 test_indicators,7684 test_indicators,
7685 breakpoints,
7529 code_actions_indicator,7686 code_actions_indicator,
7530 crease_toggles,7687 crease_toggles,
7531 crease_trailers,7688 crease_trailers,
@@ -7688,7 +7845,7 @@ pub struct EditorLayout { · 156 unmodified lines
7688 wrap_guides: SmallVec<[(Pixels, bool); 2]>,7845 wrap_guides: SmallVec<[(Pixels, bool); 2]>,
7689 indent_guides: Option<Vec<IndentGuideLayout>>,7846 indent_guides: Option<Vec<IndentGuideLayout>>,
7690 visible_display_row_range: Range<DisplayRow>,7847 visible_display_row_range: Range<DisplayRow>,
7691 active_rows: BTreeMap<DisplayRow, bool>,7848 active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
7692 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,7849 highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
7693 line_elements: SmallVec<[AnyElement; 1]>,7850 line_elements: SmallVec<[AnyElement; 1]>,
7694 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,7851 line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
@@ -7705,6 +7862,7 @@ pub struct EditorLayout { · 10 unmodified lines
7705 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,7862 selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
7706 code_actions_indicator: Option<AnyElement>,7863 code_actions_indicator: Option<AnyElement>,
7707 test_indicators: Vec<AnyElement>,7864 test_indicators: Vec<AnyElement>,
7865 breakpoints: Vec<AnyElement>,
7708 crease_toggles: Vec<Option<AnyElement>>,7866 crease_toggles: Vec<Option<AnyElement>>,
7709 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,7867 expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
7710 diff_hunk_controls: Vec<AnyElement>,7868 diff_hunk_controls: Vec<AnyElement>,
@@ -7725,7 +7883,6 @@ impl EditorLayout { · 14 unmodified lines
7725struct LineNumberLayout {7883struct LineNumberLayout {
7726 shaped_line: ShapedLine,7884 shaped_line: ShapedLine,
7727 hitbox: Option<Hitbox>,7885 hitbox: Option<Hitbox>,
7728 display_row: DisplayRow,
7729}7886}
7730 7887
7731struct ColoredRange<T> {7888struct ColoredRange<T> {
@@ -8376,6 +8533,7 @@ mod tests { · 644 unmodified lines
8376 ..Default::default()8533 ..Default::default()
8377 })8534 })
8378 .collect::<Vec<_>>(),8535 .collect::<Vec<_>>(),
8536 &BTreeMap::default(),
8379 Some(DisplayPoint::new(DisplayRow(0), 0)),8537 Some(DisplayPoint::new(DisplayRow(0), 0)),
8380 &snapshot,8538 &snapshot,
8381 window,8539 window,
crates/extension/src/extension_host_proxy.rs+3−3
@@ -4,7 +4,7 @@ use std::sync::Arc; · 3 unmodified lines
4use anyhow::Result;4use anyhow::Result;
5use fs::Fs;5use fs::Fs;
6use gpui::{App, Global, ReadGlobal, SharedString, Task};6use gpui::{App, Global, ReadGlobal, SharedString, Task};
7use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage};7use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
8use lsp::LanguageServerName;8use lsp::LanguageServerName;
9use parking_lot::RwLock;9use parking_lot::RwLock;
10 10
@@ -284,7 +284,7 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { · 273 unmodified lines
284 fn update_language_server_status(284 fn update_language_server_status(
285 &self,285 &self,
286 language_server_id: LanguageServerName,286 language_server_id: LanguageServerName,
287 status: LanguageServerBinaryStatus,287 status: BinaryStatus,
288 );288 );
289}289}
290 290
@@ -317,7 +317,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy { · 26 unmodified lines
317 fn update_language_server_status(317 fn update_language_server_status(
318 &self,318 &self,
319 language_server_id: LanguageServerName,319 language_server_id: LanguageServerName,
320 status: LanguageServerBinaryStatus,320 status: BinaryStatus,
321 ) {321 ) {
322 let Some(proxy) = self.language_server_proxy.read().clone() else {322 let Some(proxy) = self.language_server_proxy.read().clone() else {
323 return;323 return;
crates/extension_host/src/extension_store_test.rs+5−14
@@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines
8use extension::ExtensionHostProxy;8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs, RealFs};9use fs::{FakeFs, Fs, RealFs};
10use futures::{io::BufReader, AsyncReadExt, StreamExt};10use futures::{io::BufReader, AsyncReadExt, StreamExt};
11use gpui::{AppContext as _, SemanticVersion, TestAppContext};11use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};
12use http_client::{FakeHttpClient, Response};12use http_client::{FakeHttpClient, Response};
13use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};13use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
14use lsp::LanguageServerName;14use lsp::LanguageServerName;
15use node_runtime::NodeRuntime;15use node_runtime::NodeRuntime;
16use parking_lot::Mutex;16use parking_lot::Mutex;
@@ -663,18 +663,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 646 unmodified lines
663 status_updates.next().await.unwrap(),663 status_updates.next().await.unwrap(),
664 ],664 ],
665 [665 [
666 (666 (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate),
667 LanguageServerName("gleam".into()),667 (SharedString::new("gleam"), BinaryStatus::Downloading),
668 LanguageServerBinaryStatus::CheckingForUpdate668 (SharedString::new("gleam"), BinaryStatus::None)
669 ),
670 (
671 LanguageServerName("gleam".into()),
672 LanguageServerBinaryStatus::Downloading
673 ),
674 (
675 LanguageServerName("gleam".into()),
676 LanguageServerBinaryStatus::None
677 )
678 ]669 ]
679 );670 );
680 671
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
3use crate::wasm_host::WasmState;3use crate::wasm_host::WasmState;
4use anyhow::Result;4use anyhow::Result;
5use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};5use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};
6use language::LanguageServerBinaryStatus;6use language::BinaryStatus;
7use semantic_version::SemanticVersion;7use semantic_version::SemanticVersion;
8use std::sync::{Arc, OnceLock};8use std::sync::{Arc, OnceLock};
9use wasmtime::component::{Linker, Resource};9use wasmtime::component::{Linker, Resource};
@@ -132,17 +132,11 @@ impl ExtensionImports for WasmState { · 122 unmodified lines
132 status: LanguageServerInstallationStatus,132 status: LanguageServerInstallationStatus,
133 ) -> wasmtime::Result<()> {133 ) -> wasmtime::Result<()> {
134 let status = match status {134 let status = match status {
135 LanguageServerInstallationStatus::CheckingForUpdate => {135 LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
136 LanguageServerBinaryStatus::CheckingForUpdate136 LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
137 }
138 LanguageServerInstallationStatus::Downloading => {
139 LanguageServerBinaryStatus::Downloading
140 }
141 LanguageServerInstallationStatus::Cached137 LanguageServerInstallationStatus::Cached
142 | LanguageServerInstallationStatus::Downloaded => LanguageServerBinaryStatus::None,138 | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None,
143 LanguageServerInstallationStatus::Failed(error) => {139 LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
144 LanguageServerBinaryStatus::Failed { error }
145 }
146 };140 };
147 141
148 self.host142 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
8use futures::{io::BufReader, FutureExt as _};8use futures::{io::BufReader, FutureExt as _};
9use futures::{lock::Mutex, AsyncReadExt};9use futures::{lock::Mutex, AsyncReadExt};
10use language::LanguageName;10use language::LanguageName;
11use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus};11use language::{language_settings::AllLanguageSettings, BinaryStatus};
12use project::project_settings::ProjectSettings;12use project::project_settings::ProjectSettings;
13use semantic_version::SemanticVersion;13use semantic_version::SemanticVersion;
14use std::{14use std::{
@@ -474,16 +474,10 @@ impl ExtensionImports for WasmState { · 459 unmodified lines
474 status: LanguageServerInstallationStatus,474 status: LanguageServerInstallationStatus,
475 ) -> wasmtime::Result<()> {475 ) -> wasmtime::Result<()> {
476 let status = match status {476 let status = match status {
477 LanguageServerInstallationStatus::CheckingForUpdate => {477 LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
478 LanguageServerBinaryStatus::CheckingForUpdate478 LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
479 }479 LanguageServerInstallationStatus::None => BinaryStatus::None,
480 LanguageServerInstallationStatus::Downloading => {480 LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
481 LanguageServerBinaryStatus::Downloading
482 }
483 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
484 LanguageServerInstallationStatus::Failed(error) => {
485 LanguageServerBinaryStatus::Failed { error }
486 }
487 };481 };
488 482
489 self.host483 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
13};13};
14use futures::{io::BufReader, FutureExt as _};14use futures::{io::BufReader, FutureExt as _};
15use futures::{lock::Mutex, AsyncReadExt};15use futures::{lock::Mutex, AsyncReadExt};
16use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};16use language::{language_settings::AllLanguageSettings, BinaryStatus, LanguageName};
17use project::project_settings::ProjectSettings;17use project::project_settings::ProjectSettings;
18use semantic_version::SemanticVersion;18use semantic_version::SemanticVersion;
19use std::{19use std::{
@@ -692,16 +692,10 @@ impl ExtensionImports for WasmState { · 672 unmodified lines
692 status: LanguageServerInstallationStatus,692 status: LanguageServerInstallationStatus,
693 ) -> wasmtime::Result<()> {693 ) -> wasmtime::Result<()> {
694 let status = match status {694 let status = match status {
695 LanguageServerInstallationStatus::CheckingForUpdate => {695 LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
696 LanguageServerBinaryStatus::CheckingForUpdate696 LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
697 }697 LanguageServerInstallationStatus::None => BinaryStatus::None,
698 LanguageServerInstallationStatus::Downloading => {698 LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
699 LanguageServerBinaryStatus::Downloading
700 }
701 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
702 LanguageServerInstallationStatus::Failed(error) => {
703 LanguageServerBinaryStatus::Failed { error }
704 }
705 };699 };
706 700
707 self.host701 self.host
crates/feature_flags/src/feature_flags.rs+6−1
@@ -115,6 +115,11 @@ impl FeatureFlag for AutoCommand { · 114 unmodified lines
115 }115 }
116}116}
117 117
118pub struct Debugger {}
119impl FeatureFlag for Debugger {
120 const NAME: &'static str = "debugger";
121}
122
118pub trait FeatureFlagViewExt<V: 'static> {123pub trait FeatureFlagViewExt<V: 'static> {
119 fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription124 fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
120 where125 where
@@ -202,7 +207,7 @@ impl FeatureFlagAppExt for App { · 81 unmodified lines
202 fn has_flag<T: FeatureFlag>(&self) -> bool {207 fn has_flag<T: FeatureFlag>(&self) -> bool {
203 self.try_global::<FeatureFlags>()208 self.try_global::<FeatureFlags>()
204 .map(|flags| flags.has_flag::<T>())209 .map(|flags| flags.has_flag::<T>())
205 .unwrap_or(false)210 .unwrap_or_else(|| T::enabled_in_development())
206 }211 }
207 212
208 fn is_staff(&self) -> bool {213 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
2539 assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");2539 assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");
2540 });2540 });
2541 2541
2542 // Convert from branch buffer ranges to the corresoponing ranges in the2542 // Convert from branch buffer ranges to the corresponding ranges in the
2543 // base buffer.2543 // base buffer.
2544 branch.read_with(cx, |buffer, cx| {2544 branch.read_with(cx, |buffer, cx| {
2545 assert_eq!(2545 assert_eq!(
crates/language/src/language.rs+8−8
@@ -73,8 +73,8 @@ pub use buffer::Operation; · 72 unmodified lines
73pub use buffer::*;73pub use buffer::*;
74pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup};74pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
75pub use language_registry::{75pub use language_registry::{
76 AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry,76 AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
77 LanguageServerBinaryStatus, QUERY_FILENAME_PREFIXES,77 QUERY_FILENAME_PREFIXES,
78};78};
79pub use lsp::{LanguageServerId, LanguageServerName};79pub use lsp::{LanguageServerId, LanguageServerName};
80pub use outline::*;80pub use outline::*;
@@ -304,7 +304,7 @@ pub trait LspAdapterDelegate: Send + Sync { · 223 unmodified lines
304 fn worktree_id(&self) -> WorktreeId;304 fn worktree_id(&self) -> WorktreeId;
305 fn worktree_root_path(&self) -> &Path;305 fn worktree_root_path(&self) -> &Path;
306 fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;306 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);
308 async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;308 async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
309 309
310 async fn npm_package_installed_version(310 async fn npm_package_installed_version(
@@ -382,7 +382,7 @@ pub trait LspAdapter: 'static + Send + Sync { · 71 unmodified lines
382 } else {382 } else {
383 delegate.update_status(383 delegate.update_status(
384 self.name(),384 self.name(),
385 LanguageServerBinaryStatus::Failed {385 BinaryStatus::Failed {
386 error: format!("{error:?}"),386 error: format!("{error:?}"),
387 },387 },
388 );388 );
@@ -586,7 +586,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> · 197 unmodified lines
586 586
587 let name = adapter.name();587 let name = adapter.name();
588 log::info!("fetching latest version of language server {:?}", name.0);588 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);
590 590
591 let latest_version = adapter591 let latest_version = adapter
592 .fetch_latest_server_version(delegate.as_ref())592 .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
597 .await597 .await
598 {598 {
599 log::info!("language server {:?} is already installed", name.0);599 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);
601 Ok(binary)601 Ok(binary)
602 } else {602 } else {
603 log::info!("downloading language server {:?}", name.0);603 log::info!("downloading language server {:?}", name.0);
604 delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);604 delegate.update_status(adapter.name(), BinaryStatus::Downloading);
605 let binary = adapter605 let binary = adapter
606 .fetch_server_binary(latest_version, container_dir, delegate.as_ref())606 .fetch_server_binary(latest_version, container_dir, delegate.as_ref())
607 .await;607 .await;
608 608
609 delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);609 delegate.update_status(name.clone(), BinaryStatus::None);
610 binary610 binary
611 }611 }
612}612}
crates/language/src/language_registry.rs+22−16
@@ -98,7 +98,8 @@ pub struct LanguageRegistry { · 97 unmodified lines
98 state: RwLock<LanguageRegistryState>,98 state: RwLock<LanguageRegistryState>,
99 language_server_download_dir: Option<Arc<Path>>,99 language_server_download_dir: Option<Arc<Path>>,
100 executor: BackgroundExecutor,100 executor: BackgroundExecutor,
101 lsp_binary_status_tx: LspBinaryStatusSender,101 lsp_binary_status_tx: BinaryStatusSender,
102 dap_binary_status_tx: BinaryStatusSender,
102}103}
103 104
104struct LanguageRegistryState {105struct LanguageRegistryState {
@@ -130,7 +131,7 @@ pub struct FakeLanguageServerEntry { · 25 unmodified lines
130}131}
131 132
132#[derive(Clone, Debug, PartialEq, Eq)]133#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum LanguageServerBinaryStatus {134pub enum BinaryStatus {
134 None,135 None,
135 CheckingForUpdate,136 CheckingForUpdate,
136 Downloading,137 Downloading,
@@ -213,8 +214,8 @@ pub struct LanguageQueries { · 76 unmodified lines
213}214}
214 215
215#[derive(Clone, Default)]216#[derive(Clone, Default)]
216struct LspBinaryStatusSender {217struct BinaryStatusSender {
217 txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,218 txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(SharedString, BinaryStatus)>>>>,
218}219}
219 220
220pub struct LoadedLanguage {221pub struct LoadedLanguage {
@@ -247,6 +248,7 @@ impl LanguageRegistry { · 26 unmodified lines
247 }),248 }),
248 language_server_download_dir: None,249 language_server_download_dir: None,
249 lsp_binary_status_tx: Default::default(),250 lsp_binary_status_tx: Default::default(),
251 dap_binary_status_tx: Default::default(),
250 executor,252 executor,
251 };253 };
252 this.add(PLAIN_TEXT.clone());254 this.add(PLAIN_TEXT.clone());
@@ -914,12 +916,12 @@ impl LanguageRegistry { · 661 unmodified lines
914 self.state.read().all_lsp_adapters.get(name).cloned()916 self.state.read().all_lsp_adapters.get(name).cloned()
915 }917 }
916 918
917 pub fn update_lsp_status(919 pub fn update_lsp_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
918 &self,920 self.lsp_binary_status_tx.send(server_name.0, status);
919 server_name: LanguageServerName,921 }
920 status: LanguageServerBinaryStatus,922
921 ) {923 pub fn update_dap_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
922 self.lsp_binary_status_tx.send(server_name, status);924 self.dap_binary_status_tx.send(server_name.0, status);
923 }925 }
924 926
925 pub fn next_language_server_id(&self) -> LanguageServerId {927 pub fn next_language_server_id(&self) -> LanguageServerId {
@@ -974,10 +976,16 @@ impl LanguageRegistry { · 48 unmodified lines
974 976
975 pub fn language_server_binary_statuses(977 pub fn language_server_binary_statuses(
976 &self,978 &self,
977 ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {979 ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
978 self.lsp_binary_status_tx.subscribe()980 self.lsp_binary_status_tx.subscribe()
979 }981 }
980 982
983 pub fn dap_server_binary_statuses(
984 &self,
985 ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
986 self.dap_binary_status_tx.subscribe()
987 }
988
981 pub async fn delete_server_container(&self, name: LanguageServerName) {989 pub async fn delete_server_container(&self, name: LanguageServerName) {
982 log::info!("deleting server container");990 log::info!("deleting server container");
983 let Some(dir) = self.language_server_download_dir(&name) else {991 let Some(dir) = self.language_server_download_dir(&name) else {
@@ -1088,16 +1096,14 @@ impl LanguageRegistryState { · 104 unmodified lines
1088 }1096 }
1089}1097}
1090 1098
1091impl LspBinaryStatusSender {1099impl BinaryStatusSender {
1092 fn subscribe(1100 fn subscribe(&self) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
1093 &self,
1094 ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
1095 let (tx, rx) = mpsc::unbounded();1101 let (tx, rx) = mpsc::unbounded();
1096 self.txs.lock().push(tx);1102 self.txs.lock().push(tx);
1097 rx1103 rx
1098 }1104 }
1099 1105
1100 fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) {1106 fn send(&self, name: SharedString, status: BinaryStatus) {
1101 let mut txs = self.txs.lock();1107 let mut txs = self.txs.lock();
1102 txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());1108 txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
1103 }1109 }
crates/language_extension/src/extension_lsp_adapter.rs+3−3
@@ -12,8 +12,8 @@ use fs::Fs; · 11 unmodified lines
12use futures::{Future, FutureExt};12use futures::{Future, FutureExt};
13use gpui::AsyncApp;13use gpui::AsyncApp;
14use language::{14use language::{
15 CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus,15 BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
16 LanguageToolchainStore, LspAdapter, LspAdapterDelegate,16 LspAdapter, LspAdapterDelegate,
17};17};
18use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};18use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
19use serde::Serialize;19use serde::Serialize;
@@ -80,7 +80,7 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { · 60 unmodified lines
80 fn update_language_server_status(80 fn update_language_server_status(
81 &self,81 &self,
82 language_server_id: LanguageServerName,82 language_server_id: LanguageServerName,
83 status: LanguageServerBinaryStatus,83 status: BinaryStatus,
84 ) {84 ) {
85 self.language_registry85 self.language_registry
86 .update_lsp_status(language_server_id, status);86 .update_lsp_status(language_server_id, status);
crates/languages/src/json.rs+10−1
@@ -86,6 +86,7 @@ impl JsonLspAdapter { · 85 unmodified lines
86 cx,86 cx,
87 );87 );
88 let tasks_schema = task::TaskTemplates::generate_json_schema();88 let tasks_schema = task::TaskTemplates::generate_json_schema();
89 let debug_schema = task::DebugTaskFile::generate_json_schema();
89 let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();90 let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();
90 let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();91 let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
91 let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();92 let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
@@ -137,7 +138,15 @@ impl JsonLspAdapter { · 45 unmodified lines
137 )138 )
138 ],139 ],
139 "schema": snippets_schema,140 "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 },
141 ]150 ]
142 }151 }
143 })152 })
crates/paths/src/paths.rs+24−0
@@ -169,6 +169,12 @@ pub fn tasks_file() -> &'static PathBuf { · 168 unmodified lines
169 TASKS_FILE.get_or_init(|| config_dir().join("tasks.json"))169 TASKS_FILE.get_or_init(|| config_dir().join("tasks.json"))
170}170}
171 171
172/// Returns the path to the `debug.json` file.
173pub 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
172/// Returns the path to the extensions directory.178/// Returns the path to the extensions directory.
173///179///
174/// This is where installed extensions are stored.180/// This is where installed extensions are stored.
@@ -284,6 +290,14 @@ pub fn languages_dir() -> &'static PathBuf { · 109 unmodified lines
284 LANGUAGES_DIR.get_or_init(|| support_dir().join("languages"))290 LANGUAGES_DIR.get_or_init(|| support_dir().join("languages"))
285}291}
286 292
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.
296pub 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
287/// Returns the path to the Copilot directory.301/// Returns the path to the Copilot directory.
288pub fn copilot_dir() -> &'static PathBuf {302pub fn copilot_dir() -> &'static PathBuf {
289 static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();303 static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -328,5 +342,15 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path { · 38 unmodified lines
328 Path::new(".vscode/tasks.json")342 Path::new(".vscode/tasks.json")
329}343}
330 344
345/// Returns the relative path to a `launch.json` file within a project.
346pub 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.
351pub fn local_vscode_launch_file_relative_path() -> &'static Path {
352 Path::new(".vscode/launch.json")
353}
354
331/// A default editorconfig file name to use when resolving project settings.355/// A default editorconfig file name to use when resolving project settings.
332pub const EDITORCONFIG_NAME: &str = ".editorconfig";356pub const EDITORCONFIG_NAME: &str = ".editorconfig";
crates/project/Cargo.toml+10−2
@@ -22,6 +22,8 @@ test-support = [ · 21 unmodified lines
22 "prettier/test-support",22 "prettier/test-support",
23 "worktree/test-support",23 "worktree/test-support",
24 "gpui/test-support",24 "gpui/test-support",
25 "dap/test-support",
26 "dap_adapters/test-support",
25]27]
26 28
27[dependencies]29[dependencies]
@@ -30,9 +32,12 @@ anyhow.workspace = true · 2 unmodified lines
30askpass.workspace = true32askpass.workspace = true
31async-trait.workspace = true33async-trait.workspace = true
32buffer_diff.workspace = true34buffer_diff.workspace = true
35circular-buffer.workspace = true
33client.workspace = true36client.workspace = true
34clock.workspace = true37clock.workspace = true
35collections.workspace = true38collections.workspace = true
39dap.workspace = true
40dap_adapters.workspace = true
36extension.workspace = true41extension.workspace = true
37fancy-regex.workspace = true42fancy-regex.workspace = true
38fs.workspace = true43fs.workspace = true
@@ -44,6 +49,7 @@ gpui.workspace = true · 5 unmodified lines
44http_client.workspace = true49http_client.workspace = true
45image.workspace = true50image.workspace = true
46itertools.workspace = true51itertools.workspace = true
52indexmap.workspace = true
47language.workspace = true53language.workspace = true
48log.workspace = true54log.workspace = true
49lsp.workspace = true55lsp.workspace = true
@@ -81,17 +87,19 @@ worktree.workspace = true · 31 unmodified lines
81client = { workspace = true, features = ["test-support"] }87client = { workspace = true, features = ["test-support"] }
82collections = { workspace = true, features = ["test-support"] }88collections = { workspace = true, features = ["test-support"] }
83buffer_diff = { workspace = true, features = ["test-support"] }89buffer_diff = { workspace = true, features = ["test-support"] }
90dap = { workspace = true, features = ["test-support"] }
91dap_adapters = { workspace = true, features = ["test-support"] }
84env_logger.workspace = true92env_logger.workspace = true
85fs = { workspace = true, features = ["test-support"] }93fs = { workspace = true, features = ["test-support"] }
86git2.workspace = true94git2.workspace = true
87gpui = { workspace = true, features = ["test-support"] }95gpui = { workspace = true, features = ["test-support"] }
88language = { workspace = true, features = ["test-support"] }96language = { workspace = true, features = ["test-support"] }
89release_channel.workspace = true
90lsp = { workspace = true, features = ["test-support"] }97lsp = { workspace = true, features = ["test-support"] }
91prettier = { workspace = true, features = ["test-support"] }98prettier = { workspace = true, features = ["test-support"] }
92pretty_assertions.workspace = true99pretty_assertions.workspace = true
93worktree = { workspace = true, features = ["test-support"] }100release_channel.workspace = true
94rpc = { workspace = true, features = ["test-support"] }101rpc = { workspace = true, features = ["test-support"] }
95settings = { workspace = true, features = ["test-support"] }102settings = { workspace = true, features = ["test-support"] }
96unindent.workspace = true103unindent.workspace = true
97util = { workspace = true, features = ["test-support"] }104util = { workspace = true, features = ["test-support"] }
105worktree = { workspace = true, features = ["test-support"] }
crates/project/src/buffer_store.rs+31−4
@@ -85,6 +85,10 @@ enum OpenBuffer { · 84 unmodified lines
85 85
86pub enum BufferStoreEvent {86pub enum BufferStoreEvent {
87 BufferAdded(Entity<Buffer>),87 BufferAdded(Entity<Buffer>),
88 BufferOpened {
89 buffer: Entity<Buffer>,
90 project_path: ProjectPath,
91 },
88 SharedBufferClosed(proto::PeerId, BufferId),92 SharedBufferClosed(proto::PeerId, BufferId),
89 BufferDropped(BufferId),93 BufferDropped(BufferId),
90 BufferChangedFilePath {94 BufferChangedFilePath {
@@ -802,6 +806,13 @@ impl BufferStore { · 711 unmodified lines
802 }806 }
803 }807 }
804 808
809 fn as_local(&self) -> Option<&LocalBufferStore> {
810 match &self.state {
811 BufferStoreState::Local(state) => Some(state),
812 _ => None,
813 }
814 }
815
805 fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {816 fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {
806 match &mut self.state {817 match &mut self.state {
807 BufferStoreState::Local(state) => Some(state),818 BufferStoreState::Local(state) => Some(state),
@@ -829,6 +840,11 @@ impl BufferStore { · 21 unmodified lines
829 cx: &mut Context<Self>,840 cx: &mut Context<Self>,
830 ) -> Task<Result<Entity<Buffer>>> {841 ) -> Task<Result<Entity<Buffer>>> {
831 if let Some(buffer) = self.get_by_path(&project_path, cx) {842 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
832 return Task::ready(Ok(buffer));848 return Task::ready(Ok(buffer));
833 }849 }
834 850
@@ -852,12 +868,18 @@ impl BufferStore { · 17 unmodified lines
852 .insert(868 .insert(
853 cx.spawn(move |this, mut cx| async move {869 cx.spawn(move |this, mut cx| async move {
854 let load_result = load_buffer.await;870 let load_result = load_buffer.await;
855 this.update(&mut cx, |this, _cx| {871 this.update(&mut cx, |this, cx| {
856 // Record the fact that the buffer is no longer loading.872 // Record the fact that the buffer is no longer loading.
857 this.loading_buffers.remove(&project_path);873 this.loading_buffers.remove(&project_path);
858 })874
859 .ok();875 let buffer = load_result.map_err(Arc::new)?;
860 load_result.map_err(Arc::new)876 cx.emit(BufferStoreEvent::BufferOpened {
877 buffer: buffer.clone(),
878 project_path,
879 });
880
881 Ok(buffer)
882 })?
861 })883 })
862 .shared(),884 .shared(),
863 )885 )
@@ -1147,6 +1169,11 @@ impl BufferStore { · 283 unmodified lines
1147 })1169 })
1148 }1170 }
1149 1171
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
1150 pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {1177 pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {
1151 self.buffers().find_map(|buffer| {1178 self.buffers().find_map(|buffer| {
1152 let file = File::from_dyn(buffer.read(cx).file())?;1179 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
14pub mod breakpoint_store;
15pub mod dap_command;
16pub mod dap_store;
17pub mod session;
18
19pub use dap_adapters::attach_processes;
crates/project/src/debugger/README.md+350−0
@@ -0,0 +1,350 @@
1# Debugger
2
3Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages.
4DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other.
5It allows Zed to support various debuggers without needing to implement language-specific debugging logic.
6This protocol enables features like setting breakpoints, stepping through code, inspecting variables,
7and more, in a consistent manner across different programming languages and runtime environments.
8
9## Supported Debug Adapters
10
11Zed 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
27These 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
31To 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
35To 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
65Only javascript and lldb supports starting a debug session using attach.
66
67When 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
81Without 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
97This 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
111This 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
130This 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