### 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 ---------
Unified · Split
| @@ -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 | ] | ||
| @@ -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]] | ||
| 2647 | name = "circular-buffer" | ||
| 2648 | version = "1.0.0" | ||
| 2649 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2650 | checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f" | ||
| 2651 | |||
| 2647 | [[package]] | 2652 | [[package]] |
| 2648 | name = "clang-sys" | 2653 | name = "clang-sys" |
| 2649 | version = "1.8.1" | 2654 | version = "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]] | ||
| 3851 | name = "dap" | ||
| 3852 | version = "0.1.0" | ||
| 3853 | dependencies = [ | ||
| 3854 | "anyhow", | ||
| 3855 | "async-compression", | ||
| 3856 | "async-pipe", | ||
| 3857 | "async-tar", | ||
| 3858 | "async-trait", | ||
| 3859 | "client", | ||
| 3860 | "collections", | ||
| 3861 | "dap-types", | ||
| 3862 | "env_logger 0.11.7", | ||
| 3863 | "fs", | ||
| 3864 | "futures 0.3.31", | ||
| 3865 | "gpui", | ||
| 3866 | "http_client", | ||
| 3867 | "language", | ||
| 3868 | "log", | ||
| 3869 | "node_runtime", | ||
| 3870 | "parking_lot", | ||
| 3871 | "paths", | ||
| 3872 | "schemars", | ||
| 3873 | "serde", | ||
| 3874 | "serde_json", | ||
| 3875 | "settings", | ||
| 3876 | "smallvec", | ||
| 3877 | "smol", | ||
| 3878 | "task", | ||
| 3879 | "util", | ||
| 3880 | ] | ||
| 3881 | |||
| 3882 | [[package]] | ||
| 3883 | name = "dap-types" | ||
| 3884 | version = "0.0.1" | ||
| 3885 | source = "git+https://github.com/zed-industries/dap-types?rev=bfd4af0#bfd4af084bbaa5f344e6925370d7642e41d0b5b8" | ||
| 3886 | dependencies = [ | ||
| 3887 | "schemars", | ||
| 3888 | "serde", | ||
| 3889 | "serde_json", | ||
| 3890 | ] | ||
| 3891 | |||
| 3892 | [[package]] | ||
| 3893 | name = "dap_adapters" | ||
| 3894 | version = "0.1.0" | ||
| 3895 | dependencies = [ | ||
| 3896 | "anyhow", | ||
| 3897 | "async-trait", | ||
| 3898 | "dap", | ||
| 3899 | "gpui", | ||
| 3900 | "language", | ||
| 3901 | "paths", | ||
| 3902 | "regex", | ||
| 3903 | "serde", | ||
| 3904 | "serde_json", | ||
| 3905 | "sysinfo", | ||
| 3906 | "task", | ||
| 3907 | "util", | ||
| 3908 | ] | ||
| 3909 | |||
| 3842 | [[package]] | 3910 | [[package]] |
| 3843 | name = "dashmap" | 3911 | name = "dashmap" |
| 3844 | version = "5.5.3" | 3912 | version = "5.5.3" |
| @@ -3912,6 +3980,58 @@ dependencies = [ · 67 unmodified lines | |||
| 3912 | "winapi", | 3980 | "winapi", |
| 3913 | ] | 3981 | ] |
| 3914 | 3982 | ||
| 3983 | [[package]] | ||
| 3984 | name = "debugger_tools" | ||
| 3985 | version = "0.1.0" | ||
| 3986 | dependencies = [ | ||
| 3987 | "anyhow", | ||
| 3988 | "dap", | ||
| 3989 | "editor", | ||
| 3990 | "futures 0.3.31", | ||
| 3991 | "gpui", | ||
| 3992 | "project", | ||
| 3993 | "serde_json", | ||
| 3994 | "settings", | ||
| 3995 | "smol", | ||
| 3996 | "util", | ||
| 3997 | "workspace", | ||
| 3998 | ] | ||
| 3999 | |||
| 4000 | [[package]] | ||
| 4001 | name = "debugger_ui" | ||
| 4002 | version = "0.1.0" | ||
| 4003 | dependencies = [ | ||
| 4004 | "anyhow", | ||
| 4005 | "client", | ||
| 4006 | "collections", | ||
| 4007 | "command_palette_hooks", | ||
| 4008 | "dap", | ||
| 4009 | "editor", | ||
| 4010 | "env_logger 0.11.7", | ||
| 4011 | "feature_flags", | ||
| 4012 | "futures 0.3.31", | ||
| 4013 | "fuzzy", | ||
| 4014 | "gpui", | ||
| 4015 | "language", | ||
| 4016 | "log", | ||
| 4017 | "menu", | ||
| 4018 | "picker", | ||
| 4019 | "pretty_assertions", | ||
| 4020 | "project", | ||
| 4021 | "rpc", | ||
| 4022 | "serde", | ||
| 4023 | "serde_json", | ||
| 4024 | "settings", | ||
| 4025 | "sysinfo", | ||
| 4026 | "task", | ||
| 4027 | "terminal_view", | ||
| 4028 | "theme", | ||
| 4029 | "ui", | ||
| 4030 | "unindent", | ||
| 4031 | "util", | ||
| 4032 | "workspace", | ||
| 4033 | ] | ||
| 4034 | |||
| 3915 | [[package]] | 4035 | [[package]] |
| 3916 | name = "deepseek" | 4036 | name = "deepseek" |
| 3917 | version = "0.1.0" | 4037 | version = "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 | |||
| 11100 | dependencies = [ | 11226 | dependencies = [ |
| 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 | |||
| 13657 | dependencies = [ | 13784 | dependencies = [ |
| 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 | |||
| 13675 | version = "0.1.0" | 13804 | version = "0.1.0" |
| 13676 | dependencies = [ | 13805 | dependencies = [ |
| 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", |
| @@ -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 | |||
| 236 | context_server_settings = { path = "crates/context_server_settings" } | 240 | context_server_settings = { path = "crates/context_server_settings" } |
| 237 | copilot = { path = "crates/copilot" } | 241 | copilot = { path = "crates/copilot" } |
| 238 | credentials_provider = { path = "crates/credentials_provider" } | 242 | credentials_provider = { path = "crates/credentials_provider" } |
| 243 | dap = { path = "crates/dap" } | ||
| 244 | dap_adapters = { path = "crates/dap_adapters" } | ||
| 239 | db = { path = "crates/db" } | 245 | db = { path = "crates/db" } |
| 246 | debugger_ui = { path = "crates/debugger_ui" } | ||
| 247 | debugger_tools = { path = "crates/debugger_tools" } | ||
| 240 | deepseek = { path = "crates/deepseek" } | 248 | deepseek = { path = "crates/deepseek" } |
| 241 | diagnostics = { path = "crates/diagnostics" } | 249 | diagnostics = { path = "crates/diagnostics" } |
| 242 | buffer_diff = { path = "crates/buffer_diff" } | 250 | buffer_diff = { path = "crates/buffer_diff" } |
| @@ -402,6 +410,7 @@ bytes = "1.0" · 159 unmodified lines | |||
| 402 | cargo_metadata = "0.19" | 410 | cargo_metadata = "0.19" |
| 403 | cargo_toml = "0.21" | 411 | cargo_toml = "0.21" |
| 404 | chrono = { version = "0.4", features = ["serde"] } | 412 | chrono = { version = "0.4", features = ["serde"] } |
| 413 | circular-buffer = "1.0" | ||
| 405 | clap = { version = "4.4", features = ["derive"] } | 414 | clap = { version = "4.4", features = ["derive"] } |
| 406 | cocoa = "0.26" | 415 | cocoa = "0.26" |
| 407 | cocoa-foundation = "0.2.0" | 416 | cocoa-foundation = "0.2.0" |
| @@ -410,6 +419,7 @@ core-foundation = "0.9.3" · 2 unmodified lines | |||
| 410 | core-foundation-sys = "0.8.6" | 419 | core-foundation-sys = "0.8.6" |
| 411 | ctor = "0.4.0" | 420 | ctor = "0.4.0" |
| 412 | dashmap = "6.0" | 421 | dashmap = "6.0" |
| 422 | dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" } | ||
| 413 | derive_more = "0.99.17" | 423 | derive_more = "0.99.17" |
| 414 | dirs = "4.0" | 424 | dirs = "4.0" |
| 415 | ec4rs = "1.1" | 425 | ec4rs = "1.1" |
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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> | ||
| @@ -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 | { |
| @@ -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, |
| @@ -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 | { |
| @@ -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 | { |
| @@ -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": true | 330 | "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 | } |
| @@ -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 | ] | ||
| @@ -20,7 +20,6 @@ extension_host.workspace = true · 19 unmodified lines | |||
| 20 | futures.workspace = true | 20 | futures.workspace = true |
| 21 | gpui.workspace = true | 21 | gpui.workspace = true |
| 22 | language.workspace = true | 22 | language.workspace = true |
| 23 | lsp.workspace = true | ||
| 24 | project.workspace = true | 23 | project.workspace = true |
| 25 | smallvec.workspace = true | 24 | smallvec.workspace = true |
| 26 | ui.workspace = true | 25 | ui.workspace = true |
| @@ -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 | }; |
| 10 | use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId}; | 10 | use language::{BinaryStatus, LanguageRegistry, LanguageServerId}; |
| 11 | use lsp::LanguageServerName; | ||
| 12 | use project::{ | 11 | use 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 | ||
| 24 | pub enum Event { | 23 | pub 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 | ||
| 31 | pub struct ActivityIndicator { | 30 | pub 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 | ||
| 38 | struct LspStatus { | 37 | struct ServerStatus { |
| 39 | name: LanguageServerName, | 38 | name: SharedString, |
| 40 | status: LanguageServerBinaryStatus, | 39 | status: BinaryStatus, |
| 41 | } | 40 | } |
| 42 | 41 | ||
| 43 | struct PendingWork<'a> { | 42 | struct 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 | false | 167 | 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 | failed | 365 | 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(", "); |
| @@ -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))); |
| @@ -89,8 +89,11 @@ channel.workspace = true · 88 unmodified lines | |||
| 89 | client = { workspace = true, features = ["test-support"] } | 89 | client = { workspace = true, features = ["test-support"] } |
| 90 | collab_ui = { workspace = true, features = ["test-support"] } | 90 | collab_ui = { workspace = true, features = ["test-support"] } |
| 91 | collections = { workspace = true, features = ["test-support"] } | 91 | collections = { workspace = true, features = ["test-support"] } |
| 92 | command_palette_hooks.workspace = true | ||
| 92 | context_server.workspace = true | 93 | context_server.workspace = true |
| 93 | ctor.workspace = true | 94 | ctor.workspace = true |
| 95 | dap = { workspace = true, features = ["test-support"] } | ||
| 96 | debugger_ui = { workspace = true, features = ["test-support"] } | ||
| 94 | editor = { workspace = true, features = ["test-support"] } | 97 | editor = { workspace = true, features = ["test-support"] } |
| 95 | env_logger.workspace = true | 98 | env_logger.workspace = true |
| 96 | extension.workspace = true | 99 | extension.workspace = true |
| @@ -469,3 +469,14 @@ CREATE TABLE IF NOT EXISTS processed_stripe_events ( · 468 unmodified lines | |||
| 469 | ); | 469 | ); |
| 470 | 470 | ||
| 471 | CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp); | 471 | CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp); |
| 472 | |||
| 473 | CREATE TABLE IF NOT EXISTS "breakpoints" ( | ||
| 474 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 475 | "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, | ||
| 476 | "position" INTEGER NOT NULL, | ||
| 477 | "log_message" TEXT NULL, | ||
| 478 | "worktree_id" BIGINT NOT NULL, | ||
| 479 | "path" TEXT NOT NULL, | ||
| 480 | "kind" VARCHAR NOT NULL | ||
| 481 | ); | ||
| 482 | CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | CREATE TABLE IF NOT EXISTS "breakpoints" ( | ||
| 2 | "id" SERIAL PRIMARY KEY, | ||
| 3 | "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, | ||
| 4 | "position" INTEGER NOT NULL, | ||
| 5 | "log_message" TEXT NULL, | ||
| 6 | "worktree_id" BIGINT NOT NULL, | ||
| 7 | "path" TEXT NOT NULL, | ||
| 8 | "kind" VARCHAR NOT NULL | ||
| 9 | ); | ||
| 10 | |||
| 11 | CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); | ||
| @@ -1,5 +1,5 @@ | |||
| 1 | use anyhow::Context as _; | 1 | use anyhow::Context as _; |
| 2 | 2 | use collections::HashSet; | |
| 3 | use util::ResultExt; | 3 | use util::ResultExt; |
| 4 | 4 | ||
| 5 | use super::*; | 5 | use 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 | .await | 1149 | 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( |
| @@ -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. |
| 2068 | async fn start_language_server( | 2070 | async fn start_language_server( |
| 2069 | request: proto::StartLanguageServer, | 2071 | request: proto::StartLanguageServer, |
| 2070 | session: Session, | 2072 | session: Session, |
| @@ -11,6 +11,7 @@ mod channel_buffer_tests; · 10 unmodified lines | |||
| 11 | mod channel_guest_tests; | 11 | mod channel_guest_tests; |
| 12 | mod channel_message_tests; | 12 | mod channel_message_tests; |
| 13 | mod channel_tests; | 13 | mod channel_tests; |
| 14 | // mod debug_panel_tests; | ||
| 14 | mod editor_tests; | 15 | mod editor_tests; |
| 15 | mod following_tests; | 16 | mod following_tests; |
| 16 | mod git_tests; | 17 | mod git_tests; |
| @@ -0,0 +1,2454 @@ | |||
| 1 | use call::ActiveCall; | ||
| 2 | use dap::requests::{Initialize, Launch, StackTrace}; | ||
| 3 | use dap::DebugRequestType; | ||
| 4 | use dap::{requests::SetBreakpoints, SourceBreakpoint}; | ||
| 5 | use debugger_ui::debugger_panel::DebugPanel; | ||
| 6 | use debugger_ui::session::DebugSession; | ||
| 7 | use editor::Editor; | ||
| 8 | use gpui::{Entity, TestAppContext, VisualTestContext}; | ||
| 9 | use project::{Project, ProjectPath, WorktreeId}; | ||
| 10 | use serde_json::json; | ||
| 11 | use std::sync::Arc; | ||
| 12 | use std::{ | ||
| 13 | path::Path, | ||
| 14 | sync::atomic::{AtomicBool, Ordering}, | ||
| 15 | }; | ||
| 16 | use workspace::{dock::Panel, Workspace}; | ||
| 17 | |||
| 18 | use super::{TestClient, TestServer}; | ||
| 19 | |||
| 20 | pub fn init_test(cx: &mut gpui::TestAppContext) { | ||
| 21 | if std::env::var("RUST_LOG").is_ok() { | ||
| 22 | env_logger::try_init().ok(); | ||
| 23 | } | ||
| 24 | |||
| 25 | cx.update(|cx| { | ||
| 26 | theme::init(theme::LoadThemes::JustBase, cx); | ||
| 27 | command_palette_hooks::init(cx); | ||
| 28 | language::init(cx); | ||
| 29 | workspace::init_settings(cx); | ||
| 30 | project::Project::init_settings(cx); | ||
| 31 | debugger_ui::init(cx); | ||
| 32 | editor::init(cx); | ||
| 33 | }); | ||
| 34 | } | ||
| 35 | |||
| 36 | async fn add_debugger_panel(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) { | ||
| 37 | let debugger_panel = workspace | ||
| 38 | .update_in(cx, |_workspace, window, cx| { | ||
| 39 | cx.spawn_in(window, DebugPanel::load) | ||
| 40 | }) | ||
| 41 | .await | ||
| 42 | .unwrap(); | ||
| 43 | |||
| 44 | workspace.update_in(cx, |workspace, window, cx| { | ||
| 45 | workspace.add_panel(debugger_panel, window, cx); | ||
| 46 | }); | ||
| 47 | } | ||
| 48 | |||
| 49 | pub fn _active_session( | ||
| 50 | workspace: Entity<Workspace>, | ||
| 51 | cx: &mut VisualTestContext, | ||
| 52 | ) -> Entity<DebugSession> { | ||
| 53 | workspace.update_in(cx, |workspace, _window, cx| { | ||
| 54 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 55 | debug_panel | ||
| 56 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 57 | .unwrap() | ||
| 58 | }) | ||
| 59 | } | ||
| 60 | |||
| 61 | struct ZedInstance<'a> { | ||
| 62 | client: TestClient, | ||
| 63 | project: Option<Entity<Project>>, | ||
| 64 | active_call: Entity<ActiveCall>, | ||
| 65 | cx: &'a mut TestAppContext, | ||
| 66 | } | ||
| 67 | |||
| 68 | impl<'a> ZedInstance<'a> { | ||
| 69 | fn new(client: TestClient, cx: &'a mut TestAppContext) -> Self { | ||
| 70 | ZedInstance { | ||
| 71 | project: None, | ||
| 72 | client, | ||
| 73 | active_call: cx.read(ActiveCall::global), | ||
| 74 | cx, | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | async fn host_project( | ||
| 79 | &mut self, | ||
| 80 | project_files: Option<serde_json::Value>, | ||
| 81 | ) -> (u64, WorktreeId) { | ||
| 82 | let (project, worktree_id) = self.client.build_local_project("/project", self.cx).await; | ||
| 83 | self.active_call | ||
| 84 | .update(self.cx, |call, cx| call.set_location(Some(&project), cx)) | ||
| 85 | .await | ||
| 86 | .unwrap(); | ||
| 87 | |||
| 88 | if let Some(tree) = project_files { | ||
| 89 | self.client.fs().insert_tree("/project", tree).await; | ||
| 90 | } | ||
| 91 | |||
| 92 | self.project = Some(project.clone()); | ||
| 93 | |||
| 94 | let project_id = self | ||
| 95 | .active_call | ||
| 96 | .update(self.cx, |call, cx| call.share_project(project, cx)) | ||
| 97 | .await | ||
| 98 | .unwrap(); | ||
| 99 | |||
| 100 | (project_id, worktree_id) | ||
| 101 | } | ||
| 102 | |||
| 103 | async fn join_project(&mut self, project_id: u64) { | ||
| 104 | let remote_project = self.client.join_remote_project(project_id, self.cx).await; | ||
| 105 | self.project = Some(remote_project); | ||
| 106 | |||
| 107 | self.active_call | ||
| 108 | .update(self.cx, |call, cx| { | ||
| 109 | call.set_location(self.project.as_ref(), cx) | ||
| 110 | }) | ||
| 111 | .await | ||
| 112 | .unwrap(); | ||
| 113 | } | ||
| 114 | |||
| 115 | async fn expand( | ||
| 116 | &'a mut self, | ||
| 117 | ) -> ( | ||
| 118 | &'a TestClient, | ||
| 119 | Entity<Workspace>, | ||
| 120 | Entity<Project>, | ||
| 121 | &'a mut VisualTestContext, | ||
| 122 | ) { | ||
| 123 | let (workspace, cx) = self.client.build_workspace( | ||
| 124 | self.project | ||
| 125 | .as_ref() | ||
| 126 | .expect("Project should be hosted or built before expanding"), | ||
| 127 | self.cx, | ||
| 128 | ); | ||
| 129 | add_debugger_panel(&workspace, cx).await; | ||
| 130 | (&self.client, workspace, self.project.clone().unwrap(), cx) | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | async fn _setup_three_member_test<'a, 'b, 'c>( | ||
| 135 | server: &mut TestServer, | ||
| 136 | host_cx: &'a mut TestAppContext, | ||
| 137 | first_remote_cx: &'b mut TestAppContext, | ||
| 138 | second_remote_cx: &'c mut TestAppContext, | ||
| 139 | ) -> (ZedInstance<'a>, ZedInstance<'b>, ZedInstance<'c>) { | ||
| 140 | let host_client = server.create_client(host_cx, "user_host").await; | ||
| 141 | let first_remote_client = server.create_client(first_remote_cx, "user_remote_1").await; | ||
| 142 | let second_remote_client = server | ||
| 143 | .create_client(second_remote_cx, "user_remote_2") | ||
| 144 | .await; | ||
| 145 | |||
| 146 | init_test(host_cx); | ||
| 147 | init_test(first_remote_cx); | ||
| 148 | init_test(second_remote_cx); | ||
| 149 | |||
| 150 | server | ||
| 151 | .create_room(&mut [ | ||
| 152 | (&host_client, host_cx), | ||
| 153 | (&first_remote_client, first_remote_cx), | ||
| 154 | (&second_remote_client, second_remote_cx), | ||
| 155 | ]) | ||
| 156 | .await; | ||
| 157 | |||
| 158 | let host_zed = ZedInstance::new(host_client, host_cx); | ||
| 159 | let first_remote_zed = ZedInstance::new(first_remote_client, first_remote_cx); | ||
| 160 | let second_remote_zed = ZedInstance::new(second_remote_client, second_remote_cx); | ||
| 161 | |||
| 162 | (host_zed, first_remote_zed, second_remote_zed) | ||
| 163 | } | ||
| 164 | |||
| 165 | async fn setup_two_member_test<'a, 'b>( | ||
| 166 | server: &mut TestServer, | ||
| 167 | host_cx: &'a mut TestAppContext, | ||
| 168 | remote_cx: &'b mut TestAppContext, | ||
| 169 | ) -> (ZedInstance<'a>, ZedInstance<'b>) { | ||
| 170 | let host_client = server.create_client(host_cx, "user_host").await; | ||
| 171 | let remote_client = server.create_client(remote_cx, "user_remote").await; | ||
| 172 | |||
| 173 | init_test(host_cx); | ||
| 174 | init_test(remote_cx); | ||
| 175 | |||
| 176 | server | ||
| 177 | .create_room(&mut [(&host_client, host_cx), (&remote_client, remote_cx)]) | ||
| 178 | .await; | ||
| 179 | |||
| 180 | let host_zed = ZedInstance::new(host_client, host_cx); | ||
| 181 | let remote_zed = ZedInstance::new(remote_client, remote_cx); | ||
| 182 | |||
| 183 | (host_zed, remote_zed) | ||
| 184 | } | ||
| 185 | |||
| 186 | #[gpui::test] | ||
| 187 | async fn test_debug_panel_item_opens_on_remote( | ||
| 188 | host_cx: &mut TestAppContext, | ||
| 189 | remote_cx: &mut TestAppContext, | ||
| 190 | ) { | ||
| 191 | let executor = host_cx.executor(); | ||
| 192 | let mut server = TestServer::start(executor).await; | ||
| 193 | |||
| 194 | let (mut host_zed, mut remote_zed) = | ||
| 195 | setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 196 | |||
| 197 | let (host_project_id, _) = host_zed.host_project(None).await; | ||
| 198 | remote_zed.join_project(host_project_id).await; | ||
| 199 | |||
| 200 | let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 201 | let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 202 | |||
| 203 | remote_cx.run_until_parked(); | ||
| 204 | |||
| 205 | let task = host_project.update(host_cx, |project, cx| { | ||
| 206 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 207 | }); | ||
| 208 | |||
| 209 | let session = task.await.unwrap(); | ||
| 210 | let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 211 | |||
| 212 | client | ||
| 213 | .on_request::<Initialize, _>(move |_, _| { | ||
| 214 | Ok(dap::Capabilities { | ||
| 215 | supports_step_back: Some(false), | ||
| 216 | ..Default::default() | ||
| 217 | }) | ||
| 218 | }) | ||
| 219 | .await; | ||
| 220 | |||
| 221 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 222 | |||
| 223 | client | ||
| 224 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 225 | Ok(dap::StackTraceResponse { | ||
| 226 | stack_frames: Vec::default(), | ||
| 227 | total_frames: None, | ||
| 228 | }) | ||
| 229 | }) | ||
| 230 | .await; | ||
| 231 | |||
| 232 | client | ||
| 233 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 234 | reason: dap::StoppedEventReason::Pause, | ||
| 235 | description: None, | ||
| 236 | thread_id: Some(1), | ||
| 237 | preserve_focus_hint: None, | ||
| 238 | text: None, | ||
| 239 | all_threads_stopped: None, | ||
| 240 | hit_breakpoint_ids: None, | ||
| 241 | })) | ||
| 242 | .await; | ||
| 243 | |||
| 244 | host_cx.run_until_parked(); | ||
| 245 | remote_cx.run_until_parked(); | ||
| 246 | |||
| 247 | remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 248 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 249 | let _active_session = debug_panel | ||
| 250 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 251 | .unwrap(); | ||
| 252 | |||
| 253 | assert_eq!( | ||
| 254 | 1, | ||
| 255 | debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 256 | ); | ||
| 257 | // assert_eq!(client.id(), active_session.read(cx).()); | ||
| 258 | // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 259 | // todo(debugger) check selected thread id | ||
| 260 | }); | ||
| 261 | |||
| 262 | let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 263 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 264 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 265 | }) | ||
| 266 | }); | ||
| 267 | |||
| 268 | shutdown_client.await.unwrap(); | ||
| 269 | } | ||
| 270 | |||
| 271 | #[gpui::test] | ||
| 272 | async fn test_active_debug_panel_item_set_on_join_project( | ||
| 273 | host_cx: &mut TestAppContext, | ||
| 274 | remote_cx: &mut TestAppContext, | ||
| 275 | ) { | ||
| 276 | let executor = host_cx.executor(); | ||
| 277 | let mut server = TestServer::start(executor).await; | ||
| 278 | |||
| 279 | let (mut host_zed, mut remote_zed) = | ||
| 280 | setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 281 | |||
| 282 | let (host_project_id, _) = host_zed.host_project(None).await; | ||
| 283 | |||
| 284 | let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 285 | |||
| 286 | host_cx.run_until_parked(); | ||
| 287 | |||
| 288 | let task = host_project.update(host_cx, |project, cx| { | ||
| 289 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 290 | }); | ||
| 291 | |||
| 292 | let session = task.await.unwrap(); | ||
| 293 | let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 294 | |||
| 295 | client | ||
| 296 | .on_request::<Initialize, _>(move |_, _| { | ||
| 297 | Ok(dap::Capabilities { | ||
| 298 | supports_step_back: Some(false), | ||
| 299 | ..Default::default() | ||
| 300 | }) | ||
| 301 | }) | ||
| 302 | .await; | ||
| 303 | |||
| 304 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 305 | |||
| 306 | client | ||
| 307 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 308 | Ok(dap::StackTraceResponse { | ||
| 309 | stack_frames: Vec::default(), | ||
| 310 | total_frames: None, | ||
| 311 | }) | ||
| 312 | }) | ||
| 313 | .await; | ||
| 314 | |||
| 315 | client | ||
| 316 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 317 | reason: dap::StoppedEventReason::Pause, | ||
| 318 | description: None, | ||
| 319 | thread_id: Some(1), | ||
| 320 | preserve_focus_hint: None, | ||
| 321 | text: None, | ||
| 322 | all_threads_stopped: None, | ||
| 323 | hit_breakpoint_ids: None, | ||
| 324 | })) | ||
| 325 | .await; | ||
| 326 | |||
| 327 | // Give host_client time to send a debug panel item to collab server | ||
| 328 | host_cx.run_until_parked(); | ||
| 329 | |||
| 330 | remote_zed.join_project(host_project_id).await; | ||
| 331 | let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 332 | |||
| 333 | host_cx.run_until_parked(); | ||
| 334 | remote_cx.run_until_parked(); | ||
| 335 | |||
| 336 | remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 337 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 338 | let _active_session = debug_panel | ||
| 339 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 340 | .unwrap(); | ||
| 341 | |||
| 342 | assert_eq!( | ||
| 343 | 1, | ||
| 344 | debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 345 | ); | ||
| 346 | // assert_eq!(cl, active_session.read(cx).client_id()); | ||
| 347 | // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 348 | // todo(debugger) | ||
| 349 | }); | ||
| 350 | |||
| 351 | let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 352 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 353 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 354 | }) | ||
| 355 | }); | ||
| 356 | |||
| 357 | shutdown_client.await.unwrap(); | ||
| 358 | |||
| 359 | remote_cx.run_until_parked(); | ||
| 360 | |||
| 361 | // assert we don't have a debug panel item anymore because the client shutdown | ||
| 362 | remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 363 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 364 | |||
| 365 | debug_panel.update(cx, |this, cx| { | ||
| 366 | assert!(this.active_session(cx).is_none()); | ||
| 367 | assert_eq!(0, this.pane().unwrap().read(cx).items_len()); | ||
| 368 | }); | ||
| 369 | }); | ||
| 370 | } | ||
| 371 | |||
| 372 | #[gpui::test] | ||
| 373 | async fn test_debug_panel_remote_button_presses( | ||
| 374 | _host_cx: &mut TestAppContext, | ||
| 375 | _remote_cx: &mut TestAppContext, | ||
| 376 | ) { | ||
| 377 | unimplemented!("Collab is still being refactored"); | ||
| 378 | // let executor = host_cx.executor(); | ||
| 379 | // let mut server = TestServer::start(executor).await; | ||
| 380 | |||
| 381 | // let (mut host_zed, mut remote_zed) = | ||
| 382 | // setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 383 | |||
| 384 | // let (host_project_id, _) = host_zed.host_project(None).await; | ||
| 385 | // remote_zed.join_project(host_project_id).await; | ||
| 386 | |||
| 387 | // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 388 | // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 389 | |||
| 390 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 391 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 392 | // }); | ||
| 393 | |||
| 394 | // let session = task.await.unwrap(); | ||
| 395 | // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 396 | |||
| 397 | // client | ||
| 398 | // .on_request::<Initialize, _>(move |_, _| { | ||
| 399 | // Ok(dap::Capabilities { | ||
| 400 | // supports_step_back: Some(true), | ||
| 401 | // ..Default::default() | ||
| 402 | // }) | ||
| 403 | // }) | ||
| 404 | // .await; | ||
| 405 | |||
| 406 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 407 | |||
| 408 | // client | ||
| 409 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 410 | // Ok(dap::StackTraceResponse { | ||
| 411 | // stack_frames: Vec::default(), | ||
| 412 | // total_frames: None, | ||
| 413 | // }) | ||
| 414 | // }) | ||
| 415 | // .await; | ||
| 416 | |||
| 417 | // client | ||
| 418 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 419 | // reason: dap::StoppedEventReason::Pause, | ||
| 420 | // description: None, | ||
| 421 | // thread_id: Some(1), | ||
| 422 | // preserve_focus_hint: None, | ||
| 423 | // text: None, | ||
| 424 | // all_threads_stopped: None, | ||
| 425 | // hit_breakpoint_ids: None, | ||
| 426 | // })) | ||
| 427 | // .await; | ||
| 428 | |||
| 429 | // client | ||
| 430 | // .on_request::<dap::requests::Continue, _>(move |_, _| { | ||
| 431 | // Ok(dap::ContinueResponse { | ||
| 432 | // all_threads_continued: Some(true), | ||
| 433 | // }) | ||
| 434 | // }) | ||
| 435 | // .await; | ||
| 436 | |||
| 437 | // host_cx.run_until_parked(); | ||
| 438 | // remote_cx.run_until_parked(); | ||
| 439 | |||
| 440 | // let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 441 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 442 | // let active_session = debug_panel | ||
| 443 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 444 | // .unwrap(); | ||
| 445 | |||
| 446 | // assert_eq!( | ||
| 447 | // 1, | ||
| 448 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 449 | // ); | ||
| 450 | // // assert_eq!(client.id(), active_session.read(cx).client_id()); | ||
| 451 | // // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 452 | // // todo(debugger) | ||
| 453 | // active_session | ||
| 454 | // }); | ||
| 455 | |||
| 456 | // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { | ||
| 457 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 458 | // let active_session = debug_panel | ||
| 459 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 460 | // .unwrap(); | ||
| 461 | |||
| 462 | // assert_eq!( | ||
| 463 | // 1, | ||
| 464 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 465 | // ); | ||
| 466 | // // assert_eq!(client.id(), active_session.read(cx).client_id()); | ||
| 467 | // // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 468 | // // todo(debugger) | ||
| 469 | // active_session | ||
| 470 | // }); | ||
| 471 | |||
| 472 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 473 | // this.continue_thread(cx); | ||
| 474 | // }); | ||
| 475 | |||
| 476 | // host_cx.run_until_parked(); | ||
| 477 | // remote_cx.run_until_parked(); | ||
| 478 | |||
| 479 | // local_debug_item.update(host_cx, |debug_panel_item, cx| { | ||
| 480 | // assert_eq!( | ||
| 481 | // debugger_ui::debugger_panel::ThreadStatus::Running, | ||
| 482 | // debug_panel_item.thread_state().read(cx).status, | ||
| 483 | // ); | ||
| 484 | // }); | ||
| 485 | |||
| 486 | // remote_debug_item.update(remote_cx, |debug_panel_item, cx| { | ||
| 487 | // assert_eq!( | ||
| 488 | // debugger_ui::debugger_panel::ThreadStatus::Running, | ||
| 489 | // debug_panel_item.thread_state().read(cx).status, | ||
| 490 | // ); | ||
| 491 | // }); | ||
| 492 | |||
| 493 | // client | ||
| 494 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 495 | // reason: dap::StoppedEventReason::Pause, | ||
| 496 | // description: None, | ||
| 497 | // thread_id: Some(1), | ||
| 498 | // preserve_focus_hint: None, | ||
| 499 | // text: None, | ||
| 500 | // all_threads_stopped: None, | ||
| 501 | // hit_breakpoint_ids: None, | ||
| 502 | // })) | ||
| 503 | // .await; | ||
| 504 | |||
| 505 | // client | ||
| 506 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 507 | // Ok(dap::StackTraceResponse { | ||
| 508 | // stack_frames: Vec::default(), | ||
| 509 | // total_frames: None, | ||
| 510 | // }) | ||
| 511 | // }) | ||
| 512 | // .await; | ||
| 513 | |||
| 514 | // host_cx.run_until_parked(); | ||
| 515 | // remote_cx.run_until_parked(); | ||
| 516 | |||
| 517 | // local_debug_item.update(host_cx, |debug_panel_item, cx| { | ||
| 518 | // assert_eq!( | ||
| 519 | // debugger_ui::debugger_panel::ThreadStatus::Stopped, | ||
| 520 | // debug_panel_item.thread_state().read(cx).status, | ||
| 521 | // ); | ||
| 522 | // }); | ||
| 523 | |||
| 524 | // remote_debug_item.update(remote_cx, |debug_panel_item, cx| { | ||
| 525 | // assert_eq!( | ||
| 526 | // debugger_ui::debugger_panel::ThreadStatus::Stopped, | ||
| 527 | // debug_panel_item.thread_state().read(cx).status, | ||
| 528 | // ); | ||
| 529 | // }); | ||
| 530 | |||
| 531 | // client | ||
| 532 | // .on_request::<dap::requests::Continue, _>(move |_, _| { | ||
| 533 | // Ok(dap::ContinueResponse { | ||
| 534 | // all_threads_continued: Some(true), | ||
| 535 | // }) | ||
| 536 | // }) | ||
| 537 | // .await; | ||
| 538 | |||
| 539 | // local_debug_item.update(host_cx, |this, cx| { | ||
| 540 | // this.continue_thread(cx); | ||
| 541 | // }); | ||
| 542 | |||
| 543 | // host_cx.run_until_parked(); | ||
| 544 | // remote_cx.run_until_parked(); | ||
| 545 | |||
| 546 | // local_debug_item.update(host_cx, |debug_panel_item, cx| { | ||
| 547 | // assert_eq!( | ||
| 548 | // debugger_ui::debugger_panel::ThreadStatus::Running, | ||
| 549 | // debug_panel_item.thread_state().read(cx).status, | ||
| 550 | // ); | ||
| 551 | // }); | ||
| 552 | |||
| 553 | // remote_debug_item.update(remote_cx, |debug_panel_item, cx| { | ||
| 554 | // assert_eq!( | ||
| 555 | // debugger_ui::debugger_panel::ThreadStatus::Running, | ||
| 556 | // debug_panel_item.thread_state().read(cx).status, | ||
| 557 | // ); | ||
| 558 | // }); | ||
| 559 | |||
| 560 | // client | ||
| 561 | // .on_request::<dap::requests::Pause, _>(move |_, _| Ok(())) | ||
| 562 | // .await; | ||
| 563 | |||
| 564 | // client | ||
| 565 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 566 | // Ok(dap::StackTraceResponse { | ||
| 567 | // stack_frames: Vec::default(), | ||
| 568 | // total_frames: None, | ||
| 569 | // }) | ||
| 570 | // }) | ||
| 571 | // .await; | ||
| 572 | |||
| 573 | // client | ||
| 574 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 575 | // reason: dap::StoppedEventReason::Pause, | ||
| 576 | // description: None, | ||
| 577 | // thread_id: Some(1), | ||
| 578 | // preserve_focus_hint: None, | ||
| 579 | // text: None, | ||
| 580 | // all_threads_stopped: None, | ||
| 581 | // hit_breakpoint_ids: None, | ||
| 582 | // })) | ||
| 583 | // .await; | ||
| 584 | |||
| 585 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 586 | // this.pause_thread(cx); | ||
| 587 | // }); | ||
| 588 | |||
| 589 | // remote_cx.run_until_parked(); | ||
| 590 | // host_cx.run_until_parked(); | ||
| 591 | |||
| 592 | // client | ||
| 593 | // .on_request::<dap::requests::StepOut, _>(move |_, _| Ok(())) | ||
| 594 | // .await; | ||
| 595 | |||
| 596 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 597 | // this.step_out(cx); | ||
| 598 | // }); | ||
| 599 | |||
| 600 | // client | ||
| 601 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 602 | // reason: dap::StoppedEventReason::Pause, | ||
| 603 | // description: None, | ||
| 604 | // thread_id: Some(1), | ||
| 605 | // preserve_focus_hint: None, | ||
| 606 | // text: None, | ||
| 607 | // all_threads_stopped: None, | ||
| 608 | // hit_breakpoint_ids: None, | ||
| 609 | // })) | ||
| 610 | // .await; | ||
| 611 | |||
| 612 | // remote_cx.run_until_parked(); | ||
| 613 | // host_cx.run_until_parked(); | ||
| 614 | |||
| 615 | // client | ||
| 616 | // .on_request::<dap::requests::Next, _>(move |_, _| Ok(())) | ||
| 617 | // .await; | ||
| 618 | |||
| 619 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 620 | // this.step_over(cx); | ||
| 621 | // }); | ||
| 622 | |||
| 623 | // client | ||
| 624 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 625 | // reason: dap::StoppedEventReason::Pause, | ||
| 626 | // description: None, | ||
| 627 | // thread_id: Some(1), | ||
| 628 | // preserve_focus_hint: None, | ||
| 629 | // text: None, | ||
| 630 | // all_threads_stopped: None, | ||
| 631 | // hit_breakpoint_ids: None, | ||
| 632 | // })) | ||
| 633 | // .await; | ||
| 634 | |||
| 635 | // remote_cx.run_until_parked(); | ||
| 636 | // host_cx.run_until_parked(); | ||
| 637 | |||
| 638 | // client | ||
| 639 | // .on_request::<dap::requests::StepIn, _>(move |_, _| Ok(())) | ||
| 640 | // .await; | ||
| 641 | |||
| 642 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 643 | // this.step_in(cx); | ||
| 644 | // }); | ||
| 645 | |||
| 646 | // client | ||
| 647 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 648 | // reason: dap::StoppedEventReason::Pause, | ||
| 649 | // description: None, | ||
| 650 | // thread_id: Some(1), | ||
| 651 | // preserve_focus_hint: None, | ||
| 652 | // text: None, | ||
| 653 | // all_threads_stopped: None, | ||
| 654 | // hit_breakpoint_ids: None, | ||
| 655 | // })) | ||
| 656 | // .await; | ||
| 657 | |||
| 658 | // remote_cx.run_until_parked(); | ||
| 659 | // host_cx.run_until_parked(); | ||
| 660 | |||
| 661 | // client | ||
| 662 | // .on_request::<dap::requests::StepBack, _>(move |_, _| Ok(())) | ||
| 663 | // .await; | ||
| 664 | |||
| 665 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 666 | // this.step_back(cx); | ||
| 667 | // }); | ||
| 668 | |||
| 669 | // client | ||
| 670 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 671 | // reason: dap::StoppedEventReason::Pause, | ||
| 672 | // description: None, | ||
| 673 | // thread_id: Some(1), | ||
| 674 | // preserve_focus_hint: None, | ||
| 675 | // text: None, | ||
| 676 | // all_threads_stopped: None, | ||
| 677 | // hit_breakpoint_ids: None, | ||
| 678 | // })) | ||
| 679 | // .await; | ||
| 680 | |||
| 681 | // remote_cx.run_until_parked(); | ||
| 682 | // host_cx.run_until_parked(); | ||
| 683 | |||
| 684 | // remote_debug_item.update(remote_cx, |this, cx| { | ||
| 685 | // this.stop_thread(cx); | ||
| 686 | // }); | ||
| 687 | |||
| 688 | // host_cx.run_until_parked(); | ||
| 689 | // remote_cx.run_until_parked(); | ||
| 690 | |||
| 691 | // // assert we don't have a debug panel item anymore because the client shutdown | ||
| 692 | // remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 693 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 694 | |||
| 695 | // debug_panel.update(cx, |this, cx| { | ||
| 696 | // assert!(this.active_session(cx).is_none()); | ||
| 697 | // assert_eq!(0, this.pane().unwrap().read(cx).items_len()); | ||
| 698 | // }); | ||
| 699 | // }); | ||
| 700 | } | ||
| 701 | |||
| 702 | #[gpui::test] | ||
| 703 | async fn test_restart_stack_frame(_host_cx: &mut TestAppContext, _remote_cx: &mut TestAppContext) { | ||
| 704 | unimplemented!("Collab is still being refactored"); | ||
| 705 | // let executor = host_cx.executor(); | ||
| 706 | // let mut server = TestServer::start(executor).await; | ||
| 707 | |||
| 708 | // let (mut host_zed, mut remote_zed) = | ||
| 709 | // setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 710 | |||
| 711 | // let (host_project_id, _) = host_zed.host_project(None).await; | ||
| 712 | // remote_zed.join_project(host_project_id).await; | ||
| 713 | |||
| 714 | // let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 715 | // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 716 | |||
| 717 | // let called_restart_frame = Arc::new(AtomicBool::new(false)); | ||
| 718 | |||
| 719 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 720 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 721 | // }); | ||
| 722 | |||
| 723 | // let session = task.await.unwrap(); | ||
| 724 | // let client = session.read(cx).adapter_client().unwrap(); | ||
| 725 | |||
| 726 | // client | ||
| 727 | // .on_request::<Initialize, _>(move |_, _| { | ||
| 728 | // Ok(dap::Capabilities { | ||
| 729 | // supports_restart_frame: Some(true), | ||
| 730 | // ..Default::default() | ||
| 731 | // }) | ||
| 732 | // }) | ||
| 733 | // .await; | ||
| 734 | |||
| 735 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 736 | |||
| 737 | // let stack_frames = vec![StackFrame { | ||
| 738 | // id: 1, | ||
| 739 | // name: "Stack Frame 1".into(), | ||
| 740 | // source: Some(dap::Source { | ||
| 741 | // name: Some("test.js".into()), | ||
| 742 | // path: Some("/project/src/test.js".into()), | ||
| 743 | // source_reference: None, | ||
| 744 | // presentation_hint: None, | ||
| 745 | // origin: None, | ||
| 746 | // sources: None, | ||
| 747 | // adapter_data: None, | ||
| 748 | // checksums: None, | ||
| 749 | // }), | ||
| 750 | // line: 3, | ||
| 751 | // column: 1, | ||
| 752 | // end_line: None, | ||
| 753 | // end_column: None, | ||
| 754 | // can_restart: None, | ||
| 755 | // instruction_pointer_reference: None, | ||
| 756 | // module_id: None, | ||
| 757 | // presentation_hint: None, | ||
| 758 | // }]; | ||
| 759 | |||
| 760 | // client | ||
| 761 | // .on_request::<StackTrace, _>({ | ||
| 762 | // let stack_frames = Arc::new(stack_frames.clone()); | ||
| 763 | // move |_, args| { | ||
| 764 | // assert_eq!(1, args.thread_id); | ||
| 765 | |||
| 766 | // Ok(dap::StackTraceResponse { | ||
| 767 | // stack_frames: (*stack_frames).clone(), | ||
| 768 | // total_frames: None, | ||
| 769 | // }) | ||
| 770 | // } | ||
| 771 | // }) | ||
| 772 | // .await; | ||
| 773 | |||
| 774 | // client | ||
| 775 | // .on_request::<RestartFrame, _>({ | ||
| 776 | // let called_restart_frame = called_restart_frame.clone(); | ||
| 777 | // move |_, args| { | ||
| 778 | // assert_eq!(1, args.frame_id); | ||
| 779 | |||
| 780 | // called_restart_frame.store(true, Ordering::SeqCst); | ||
| 781 | |||
| 782 | // Ok(()) | ||
| 783 | // } | ||
| 784 | // }) | ||
| 785 | // .await; | ||
| 786 | |||
| 787 | // client | ||
| 788 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 789 | // reason: dap::StoppedEventReason::Pause, | ||
| 790 | // description: None, | ||
| 791 | // thread_id: Some(1), | ||
| 792 | // preserve_focus_hint: None, | ||
| 793 | // text: None, | ||
| 794 | // all_threads_stopped: None, | ||
| 795 | // hit_breakpoint_ids: None, | ||
| 796 | // })) | ||
| 797 | // .await; | ||
| 798 | |||
| 799 | // host_cx.run_until_parked(); | ||
| 800 | // remote_cx.run_until_parked(); | ||
| 801 | |||
| 802 | // // try to restart stack frame 1 from the guest side | ||
| 803 | // remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 804 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 805 | // let active_session = debug_panel | ||
| 806 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 807 | // .unwrap(); | ||
| 808 | |||
| 809 | // active_session.update(cx, |debug_panel_item, cx| { | ||
| 810 | // debug_panel_item | ||
| 811 | // .stack_frame_list() | ||
| 812 | // .update(cx, |stack_frame_list, cx| { | ||
| 813 | // stack_frame_list.restart_stack_frame(1, cx); | ||
| 814 | // }); | ||
| 815 | // }); | ||
| 816 | // }); | ||
| 817 | |||
| 818 | // host_cx.run_until_parked(); | ||
| 819 | // remote_cx.run_until_parked(); | ||
| 820 | |||
| 821 | // assert!( | ||
| 822 | // called_restart_frame.load(std::sync::atomic::Ordering::SeqCst), | ||
| 823 | // "Restart stack frame was not called" | ||
| 824 | // ); | ||
| 825 | |||
| 826 | // let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 827 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 828 | // dap_store.shutdown_session(&session.read(cx).session_id(), cx) | ||
| 829 | // }) | ||
| 830 | // }); | ||
| 831 | |||
| 832 | // shutdown_client.await.unwrap(); | ||
| 833 | } | ||
| 834 | |||
| 835 | #[gpui::test] | ||
| 836 | async fn test_updated_breakpoints_send_to_dap( | ||
| 837 | host_cx: &mut TestAppContext, | ||
| 838 | remote_cx: &mut TestAppContext, | ||
| 839 | ) { | ||
| 840 | let executor = host_cx.executor(); | ||
| 841 | let mut server = TestServer::start(executor).await; | ||
| 842 | |||
| 843 | let (mut host_zed, mut remote_zed) = | ||
| 844 | setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 845 | |||
| 846 | let (host_project_id, worktree_id) = host_zed | ||
| 847 | .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) | ||
| 848 | .await; | ||
| 849 | |||
| 850 | remote_zed.join_project(host_project_id).await; | ||
| 851 | |||
| 852 | let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 853 | let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 854 | |||
| 855 | let project_path = ProjectPath { | ||
| 856 | worktree_id, | ||
| 857 | path: Arc::from(Path::new(&"test.txt")), | ||
| 858 | }; | ||
| 859 | |||
| 860 | let task = host_project.update(host_cx, |project, cx| { | ||
| 861 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 862 | }); | ||
| 863 | |||
| 864 | let session = task.await.unwrap(); | ||
| 865 | let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 866 | |||
| 867 | client | ||
| 868 | .on_request::<Initialize, _>(move |_, _| { | ||
| 869 | Ok(dap::Capabilities { | ||
| 870 | supports_restart_frame: Some(true), | ||
| 871 | ..Default::default() | ||
| 872 | }) | ||
| 873 | }) | ||
| 874 | .await; | ||
| 875 | |||
| 876 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 877 | client | ||
| 878 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 879 | Ok(dap::StackTraceResponse { | ||
| 880 | stack_frames: Vec::default(), | ||
| 881 | total_frames: None, | ||
| 882 | }) | ||
| 883 | }) | ||
| 884 | .await; | ||
| 885 | |||
| 886 | let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 887 | client | ||
| 888 | .on_request::<SetBreakpoints, _>({ | ||
| 889 | let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 890 | move |_, args| { | ||
| 891 | assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 892 | assert_eq!( | ||
| 893 | vec![SourceBreakpoint { | ||
| 894 | line: 3, | ||
| 895 | column: None, | ||
| 896 | condition: None, | ||
| 897 | hit_condition: None, | ||
| 898 | log_message: None, | ||
| 899 | mode: None | ||
| 900 | }], | ||
| 901 | args.breakpoints.unwrap() | ||
| 902 | ); | ||
| 903 | // assert!(!args.source_modified.unwrap()); | ||
| 904 | // todo(debugger): Implement source_modified handling | ||
| 905 | |||
| 906 | called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 907 | |||
| 908 | Ok(dap::SetBreakpointsResponse { | ||
| 909 | breakpoints: Vec::default(), | ||
| 910 | }) | ||
| 911 | } | ||
| 912 | }) | ||
| 913 | .await; | ||
| 914 | |||
| 915 | client | ||
| 916 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 917 | reason: dap::StoppedEventReason::Pause, | ||
| 918 | description: None, | ||
| 919 | thread_id: Some(1), | ||
| 920 | preserve_focus_hint: None, | ||
| 921 | text: None, | ||
| 922 | all_threads_stopped: None, | ||
| 923 | hit_breakpoint_ids: None, | ||
| 924 | })) | ||
| 925 | .await; | ||
| 926 | |||
| 927 | host_cx.run_until_parked(); | ||
| 928 | remote_cx.run_until_parked(); | ||
| 929 | |||
| 930 | // Client B opens an editor. | ||
| 931 | let editor_b = remote_workspace | ||
| 932 | .update_in(remote_cx, |workspace, window, cx| { | ||
| 933 | workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 934 | }) | ||
| 935 | .await | ||
| 936 | .unwrap() | ||
| 937 | .downcast::<Editor>() | ||
| 938 | .unwrap(); | ||
| 939 | |||
| 940 | editor_b.update_in(remote_cx, |editor, window, cx| { | ||
| 941 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 942 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 943 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 944 | }); | ||
| 945 | |||
| 946 | // Client A opens an editor. | ||
| 947 | let editor_a = host_workspace | ||
| 948 | .update_in(host_cx, |workspace, window, cx| { | ||
| 949 | workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 950 | }) | ||
| 951 | .await | ||
| 952 | .unwrap() | ||
| 953 | .downcast::<Editor>() | ||
| 954 | .unwrap(); | ||
| 955 | |||
| 956 | host_cx.run_until_parked(); | ||
| 957 | remote_cx.run_until_parked(); | ||
| 958 | |||
| 959 | let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 960 | client | ||
| 961 | .on_request::<SetBreakpoints, _>({ | ||
| 962 | let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 963 | move |_, args| { | ||
| 964 | assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 965 | assert!(args.breakpoints.unwrap().is_empty()); | ||
| 966 | // assert!(!args.source_modified.unwrap()); | ||
| 967 | // todo(debugger) Implement source modified support | ||
| 968 | |||
| 969 | called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 970 | |||
| 971 | Ok(dap::SetBreakpointsResponse { | ||
| 972 | breakpoints: Vec::default(), | ||
| 973 | }) | ||
| 974 | } | ||
| 975 | }) | ||
| 976 | .await; | ||
| 977 | |||
| 978 | // remove the breakpoint that client B added | ||
| 979 | editor_a.update_in(host_cx, |editor, window, cx| { | ||
| 980 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 981 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 982 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 983 | }); | ||
| 984 | |||
| 985 | host_cx.run_until_parked(); | ||
| 986 | remote_cx.run_until_parked(); | ||
| 987 | |||
| 988 | assert!( | ||
| 989 | called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 990 | "SetBreakpoint request must be called" | ||
| 991 | ); | ||
| 992 | |||
| 993 | let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 994 | client | ||
| 995 | .on_request::<SetBreakpoints, _>({ | ||
| 996 | let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 997 | move |_, args| { | ||
| 998 | assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 999 | let mut breakpoints = args.breakpoints.unwrap(); | ||
| 1000 | breakpoints.sort_by_key(|b| b.line); | ||
| 1001 | assert_eq!( | ||
| 1002 | vec![ | ||
| 1003 | SourceBreakpoint { | ||
| 1004 | line: 2, | ||
| 1005 | column: None, | ||
| 1006 | condition: None, | ||
| 1007 | hit_condition: None, | ||
| 1008 | log_message: None, | ||
| 1009 | mode: None | ||
| 1010 | }, | ||
| 1011 | SourceBreakpoint { | ||
| 1012 | line: 3, | ||
| 1013 | column: None, | ||
| 1014 | condition: None, | ||
| 1015 | hit_condition: None, | ||
| 1016 | log_message: None, | ||
| 1017 | mode: None | ||
| 1018 | } | ||
| 1019 | ], | ||
| 1020 | breakpoints | ||
| 1021 | ); | ||
| 1022 | // assert!(!args.source_modified.unwrap()); | ||
| 1023 | // todo(debugger) Implement source modified support | ||
| 1024 | |||
| 1025 | called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 1026 | |||
| 1027 | Ok(dap::SetBreakpointsResponse { | ||
| 1028 | breakpoints: Vec::default(), | ||
| 1029 | }) | ||
| 1030 | } | ||
| 1031 | }) | ||
| 1032 | .await; | ||
| 1033 | |||
| 1034 | // Add our own breakpoint now | ||
| 1035 | editor_a.update_in(host_cx, |editor, window, cx| { | ||
| 1036 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 1037 | editor.move_up(&editor::actions::MoveUp, window, cx); | ||
| 1038 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 1039 | }); | ||
| 1040 | |||
| 1041 | host_cx.run_until_parked(); | ||
| 1042 | remote_cx.run_until_parked(); | ||
| 1043 | |||
| 1044 | assert!( | ||
| 1045 | called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1046 | "SetBreakpoint request must be called" | ||
| 1047 | ); | ||
| 1048 | |||
| 1049 | let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 1050 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 1051 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 1052 | }) | ||
| 1053 | }); | ||
| 1054 | |||
| 1055 | shutdown_client.await.unwrap(); | ||
| 1056 | } | ||
| 1057 | |||
| 1058 | #[gpui::test] | ||
| 1059 | async fn test_module_list( | ||
| 1060 | _host_cx: &mut TestAppContext, | ||
| 1061 | _remote_cx: &mut TestAppContext, | ||
| 1062 | _late_join_cx: &mut TestAppContext, | ||
| 1063 | ) { | ||
| 1064 | unimplemented!("Collab is still being refactored"); | ||
| 1065 | // let executor = host_cx.executor(); | ||
| 1066 | // let mut server = TestServer::start(executor).await; | ||
| 1067 | |||
| 1068 | // let (mut host_zed, mut remote_zed, mut late_join_zed) = | ||
| 1069 | // setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; | ||
| 1070 | |||
| 1071 | // let (host_project_id, _worktree_id) = host_zed.host_project(None).await; | ||
| 1072 | |||
| 1073 | // remote_zed.join_project(host_project_id).await; | ||
| 1074 | |||
| 1075 | // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 1076 | // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 1077 | |||
| 1078 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 1079 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 1080 | // }); | ||
| 1081 | |||
| 1082 | // let session = task.await.unwrap(); | ||
| 1083 | // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 1084 | |||
| 1085 | // let called_initialize = Arc::new(AtomicBool::new(false)); | ||
| 1086 | |||
| 1087 | // client | ||
| 1088 | // .on_request::<Initialize, _>({ | ||
| 1089 | // let called_initialize = called_initialize.clone(); | ||
| 1090 | // move |_, _| { | ||
| 1091 | // called_initialize.store(true, Ordering::SeqCst); | ||
| 1092 | // Ok(dap::Capabilities { | ||
| 1093 | // supports_restart_frame: Some(true), | ||
| 1094 | // supports_modules_request: Some(true), | ||
| 1095 | // ..Default::default() | ||
| 1096 | // }) | ||
| 1097 | // } | ||
| 1098 | // }) | ||
| 1099 | // .await; | ||
| 1100 | |||
| 1101 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 1102 | // client | ||
| 1103 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 1104 | // Ok(dap::StackTraceResponse { | ||
| 1105 | // stack_frames: Vec::default(), | ||
| 1106 | // total_frames: None, | ||
| 1107 | // }) | ||
| 1108 | // }) | ||
| 1109 | // .await; | ||
| 1110 | |||
| 1111 | // let called_modules = Arc::new(AtomicBool::new(false)); | ||
| 1112 | // let modules = vec![ | ||
| 1113 | // dap::Module { | ||
| 1114 | // id: dap::ModuleId::Number(1), | ||
| 1115 | // name: "First Module".into(), | ||
| 1116 | // address_range: None, | ||
| 1117 | // date_time_stamp: None, | ||
| 1118 | // path: None, | ||
| 1119 | // symbol_file_path: None, | ||
| 1120 | // symbol_status: None, | ||
| 1121 | // version: None, | ||
| 1122 | // is_optimized: None, | ||
| 1123 | // is_user_code: None, | ||
| 1124 | // }, | ||
| 1125 | // dap::Module { | ||
| 1126 | // id: dap::ModuleId::Number(2), | ||
| 1127 | // name: "Second Module".into(), | ||
| 1128 | // address_range: None, | ||
| 1129 | // date_time_stamp: None, | ||
| 1130 | // path: None, | ||
| 1131 | // symbol_file_path: None, | ||
| 1132 | // symbol_status: None, | ||
| 1133 | // version: None, | ||
| 1134 | // is_optimized: None, | ||
| 1135 | // is_user_code: None, | ||
| 1136 | // }, | ||
| 1137 | // ]; | ||
| 1138 | |||
| 1139 | // client | ||
| 1140 | // .on_request::<dap::requests::Modules, _>({ | ||
| 1141 | // let called_modules = called_modules.clone(); | ||
| 1142 | // let modules = modules.clone(); | ||
| 1143 | // move |_, _| unsafe { | ||
| 1144 | // static mut REQUEST_COUNT: i32 = 1; | ||
| 1145 | // assert_eq!( | ||
| 1146 | // 1, REQUEST_COUNT, | ||
| 1147 | // "This request should only be called once from the host" | ||
| 1148 | // ); | ||
| 1149 | // REQUEST_COUNT += 1; | ||
| 1150 | // called_modules.store(true, Ordering::SeqCst); | ||
| 1151 | |||
| 1152 | // Ok(dap::ModulesResponse { | ||
| 1153 | // modules: modules.clone(), | ||
| 1154 | // total_modules: Some(2u64), | ||
| 1155 | // }) | ||
| 1156 | // } | ||
| 1157 | // }) | ||
| 1158 | // .await; | ||
| 1159 | |||
| 1160 | // host_cx.run_until_parked(); | ||
| 1161 | // remote_cx.run_until_parked(); | ||
| 1162 | |||
| 1163 | // assert!( | ||
| 1164 | // called_initialize.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1165 | // "Request Initialize must be called" | ||
| 1166 | // ); | ||
| 1167 | |||
| 1168 | // client | ||
| 1169 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 1170 | // reason: dap::StoppedEventReason::Pause, | ||
| 1171 | // description: None, | ||
| 1172 | // thread_id: Some(1), | ||
| 1173 | // preserve_focus_hint: None, | ||
| 1174 | // text: None, | ||
| 1175 | // all_threads_stopped: None, | ||
| 1176 | // hit_breakpoint_ids: None, | ||
| 1177 | // })) | ||
| 1178 | // .await; | ||
| 1179 | |||
| 1180 | // host_cx.run_until_parked(); | ||
| 1181 | // remote_cx.run_until_parked(); | ||
| 1182 | |||
| 1183 | // assert!( | ||
| 1184 | // called_modules.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1185 | // "Request Modules must be called" | ||
| 1186 | // ); | ||
| 1187 | |||
| 1188 | // host_workspace.update(host_cx, |workspace, cx| { | ||
| 1189 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1190 | // let debug_panel_item = debug_panel | ||
| 1191 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 1192 | // .unwrap(); | ||
| 1193 | |||
| 1194 | // debug_panel_item.update(cx, |item, cx| { | ||
| 1195 | // assert_eq!( | ||
| 1196 | // true, | ||
| 1197 | // item.capabilities(cx).supports_modules_request.unwrap(), | ||
| 1198 | // "Local supports modules request should be true" | ||
| 1199 | // ); | ||
| 1200 | |||
| 1201 | // let local_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); | ||
| 1202 | |||
| 1203 | // assert_eq!( | ||
| 1204 | // 2usize, | ||
| 1205 | // local_module_list.len(), | ||
| 1206 | // "Local module list should have two items in it" | ||
| 1207 | // ); | ||
| 1208 | // assert_eq!( | ||
| 1209 | // modules.clone(), | ||
| 1210 | // local_module_list, | ||
| 1211 | // "Local module list should match module list from response" | ||
| 1212 | // ); | ||
| 1213 | // }) | ||
| 1214 | // }); | ||
| 1215 | |||
| 1216 | // remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 1217 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1218 | // let debug_panel_item = debug_panel | ||
| 1219 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 1220 | // .unwrap(); | ||
| 1221 | |||
| 1222 | // debug_panel_item.update(cx, |item, cx| { | ||
| 1223 | // assert_eq!( | ||
| 1224 | // true, | ||
| 1225 | // item.capabilities(cx).supports_modules_request.unwrap(), | ||
| 1226 | // "Remote capabilities supports modules request should be true" | ||
| 1227 | // ); | ||
| 1228 | // let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); | ||
| 1229 | |||
| 1230 | // assert_eq!( | ||
| 1231 | // 2usize, | ||
| 1232 | // remote_module_list.len(), | ||
| 1233 | // "Remote module list should have two items in it" | ||
| 1234 | // ); | ||
| 1235 | // assert_eq!( | ||
| 1236 | // modules.clone(), | ||
| 1237 | // remote_module_list, | ||
| 1238 | // "Remote module list should match module list from response" | ||
| 1239 | // ); | ||
| 1240 | // }) | ||
| 1241 | // }); | ||
| 1242 | |||
| 1243 | // late_join_zed.join_project(host_project_id).await; | ||
| 1244 | // let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = | ||
| 1245 | // late_join_zed.expand().await; | ||
| 1246 | |||
| 1247 | // late_join_workspace.update(late_join_cx, |workspace, cx| { | ||
| 1248 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1249 | // let debug_panel_item = debug_panel | ||
| 1250 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 1251 | // .unwrap(); | ||
| 1252 | |||
| 1253 | // debug_panel_item.update(cx, |item, cx| { | ||
| 1254 | // assert_eq!( | ||
| 1255 | // true, | ||
| 1256 | // item.capabilities(cx).supports_modules_request.unwrap(), | ||
| 1257 | // "Remote (mid session join) capabilities supports modules request should be true" | ||
| 1258 | // ); | ||
| 1259 | // let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); | ||
| 1260 | |||
| 1261 | // assert_eq!( | ||
| 1262 | // 2usize, | ||
| 1263 | // remote_module_list.len(), | ||
| 1264 | // "Remote (mid session join) module list should have two items in it" | ||
| 1265 | // ); | ||
| 1266 | // assert_eq!( | ||
| 1267 | // modules.clone(), | ||
| 1268 | // remote_module_list, | ||
| 1269 | // "Remote (mid session join) module list should match module list from response" | ||
| 1270 | // ); | ||
| 1271 | // }) | ||
| 1272 | // }); | ||
| 1273 | |||
| 1274 | // let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 1275 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 1276 | // dap_store.shutdown_session(&session.read(cx).id(), cx) | ||
| 1277 | // }) | ||
| 1278 | // }); | ||
| 1279 | |||
| 1280 | // shutdown_client.await.unwrap(); | ||
| 1281 | } | ||
| 1282 | |||
| 1283 | // #[gpui::test] | ||
| 1284 | // async fn test_variable_list( | ||
| 1285 | // host_cx: &mut TestAppContext, | ||
| 1286 | // remote_cx: &mut TestAppContext, | ||
| 1287 | // late_join_cx: &mut TestAppContext, | ||
| 1288 | // ) { | ||
| 1289 | // let executor = host_cx.executor(); | ||
| 1290 | // let mut server = TestServer::start(executor).await; | ||
| 1291 | |||
| 1292 | // let (mut host_zed, mut remote_zed, mut late_join_zed) = | ||
| 1293 | // setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; | ||
| 1294 | |||
| 1295 | // let (host_project_id, _worktree_id) = host_zed | ||
| 1296 | // .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) | ||
| 1297 | // .await; | ||
| 1298 | |||
| 1299 | // remote_zed.join_project(host_project_id).await; | ||
| 1300 | |||
| 1301 | // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 1302 | // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 1303 | |||
| 1304 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 1305 | // project.start_debug_session( | ||
| 1306 | // dap::DebugAdapterConfig { | ||
| 1307 | // label: "test config".into(), | ||
| 1308 | // kind: dap::DebugAdapterKind::Fake, | ||
| 1309 | // request: dap::DebugRequestType::Launch, | ||
| 1310 | // program: None, | ||
| 1311 | // cwd: None, | ||
| 1312 | // initialize_args: None, | ||
| 1313 | // }, | ||
| 1314 | // cx, | ||
| 1315 | // ) | ||
| 1316 | // }); | ||
| 1317 | |||
| 1318 | // let (session, client) = task.await.unwrap(); | ||
| 1319 | |||
| 1320 | // client | ||
| 1321 | // .on_request::<Initialize, _>(move |_, _| { | ||
| 1322 | // Ok(dap::Capabilities { | ||
| 1323 | // supports_step_back: Some(true), | ||
| 1324 | // ..Default::default() | ||
| 1325 | // }) | ||
| 1326 | // }) | ||
| 1327 | // .await; | ||
| 1328 | |||
| 1329 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 1330 | |||
| 1331 | // let stack_frames = vec![dap::StackFrame { | ||
| 1332 | // id: 1, | ||
| 1333 | // name: "Stack Frame 1".into(), | ||
| 1334 | // source: Some(dap::Source { | ||
| 1335 | // name: Some("test.js".into()), | ||
| 1336 | // path: Some("/project/src/test.js".into()), | ||
| 1337 | // source_reference: None, | ||
| 1338 | // presentation_hint: None, | ||
| 1339 | // origin: None, | ||
| 1340 | // sources: None, | ||
| 1341 | // adapter_data: None, | ||
| 1342 | // checksums: None, | ||
| 1343 | // }), | ||
| 1344 | // line: 1, | ||
| 1345 | // column: 1, | ||
| 1346 | // end_line: None, | ||
| 1347 | // end_column: None, | ||
| 1348 | // can_restart: None, | ||
| 1349 | // instruction_pointer_reference: None, | ||
| 1350 | // module_id: None, | ||
| 1351 | // presentation_hint: None, | ||
| 1352 | // }]; | ||
| 1353 | |||
| 1354 | // let scopes = vec![Scope { | ||
| 1355 | // name: "Scope 1".into(), | ||
| 1356 | // presentation_hint: None, | ||
| 1357 | // variables_reference: 1, | ||
| 1358 | // named_variables: None, | ||
| 1359 | // indexed_variables: None, | ||
| 1360 | // expensive: false, | ||
| 1361 | // source: None, | ||
| 1362 | // line: None, | ||
| 1363 | // column: None, | ||
| 1364 | // end_line: None, | ||
| 1365 | // end_column: None, | ||
| 1366 | // }]; | ||
| 1367 | |||
| 1368 | // let variable_1 = Variable { | ||
| 1369 | // name: "variable 1".into(), | ||
| 1370 | // value: "1".into(), | ||
| 1371 | // type_: None, | ||
| 1372 | // presentation_hint: None, | ||
| 1373 | // evaluate_name: None, | ||
| 1374 | // variables_reference: 2, | ||
| 1375 | // named_variables: None, | ||
| 1376 | // indexed_variables: None, | ||
| 1377 | // memory_reference: None, | ||
| 1378 | // }; | ||
| 1379 | |||
| 1380 | // let variable_2 = Variable { | ||
| 1381 | // name: "variable 2".into(), | ||
| 1382 | // value: "2".into(), | ||
| 1383 | // type_: None, | ||
| 1384 | // presentation_hint: None, | ||
| 1385 | // evaluate_name: None, | ||
| 1386 | // variables_reference: 3, | ||
| 1387 | // named_variables: None, | ||
| 1388 | // indexed_variables: None, | ||
| 1389 | // memory_reference: None, | ||
| 1390 | // }; | ||
| 1391 | |||
| 1392 | // let variable_3 = Variable { | ||
| 1393 | // name: "variable 3".into(), | ||
| 1394 | // value: "hello world".into(), | ||
| 1395 | // type_: None, | ||
| 1396 | // presentation_hint: None, | ||
| 1397 | // evaluate_name: None, | ||
| 1398 | // variables_reference: 4, | ||
| 1399 | // named_variables: None, | ||
| 1400 | // indexed_variables: None, | ||
| 1401 | // memory_reference: None, | ||
| 1402 | // }; | ||
| 1403 | |||
| 1404 | // let variable_4 = Variable { | ||
| 1405 | // name: "variable 4".into(), | ||
| 1406 | // value: "hello world this is the final variable".into(), | ||
| 1407 | // type_: None, | ||
| 1408 | // presentation_hint: None, | ||
| 1409 | // evaluate_name: None, | ||
| 1410 | // variables_reference: 0, | ||
| 1411 | // named_variables: None, | ||
| 1412 | // indexed_variables: None, | ||
| 1413 | // memory_reference: None, | ||
| 1414 | // }; | ||
| 1415 | |||
| 1416 | // client | ||
| 1417 | // .on_request::<StackTrace, _>({ | ||
| 1418 | // let stack_frames = std::sync::Arc::new(stack_frames.clone()); | ||
| 1419 | // move |_, args| { | ||
| 1420 | // assert_eq!(1, args.thread_id); | ||
| 1421 | |||
| 1422 | // Ok(dap::StackTraceResponse { | ||
| 1423 | // stack_frames: (*stack_frames).clone(), | ||
| 1424 | // total_frames: None, | ||
| 1425 | // }) | ||
| 1426 | // } | ||
| 1427 | // }) | ||
| 1428 | // .await; | ||
| 1429 | |||
| 1430 | // client | ||
| 1431 | // .on_request::<Scopes, _>({ | ||
| 1432 | // let scopes = Arc::new(scopes.clone()); | ||
| 1433 | // move |_, args| { | ||
| 1434 | // assert_eq!(1, args.frame_id); | ||
| 1435 | |||
| 1436 | // Ok(dap::ScopesResponse { | ||
| 1437 | // scopes: (*scopes).clone(), | ||
| 1438 | // }) | ||
| 1439 | // } | ||
| 1440 | // }) | ||
| 1441 | // .await; | ||
| 1442 | |||
| 1443 | // let first_variable_request = vec![variable_1.clone(), variable_2.clone()]; | ||
| 1444 | |||
| 1445 | // client | ||
| 1446 | // .on_request::<Variables, _>({ | ||
| 1447 | // move |_, args| { | ||
| 1448 | // assert_eq!(1, args.variables_reference); | ||
| 1449 | |||
| 1450 | // Ok(dap::VariablesResponse { | ||
| 1451 | // variables: first_variable_request.clone(), | ||
| 1452 | // }) | ||
| 1453 | // } | ||
| 1454 | // }) | ||
| 1455 | // .await; | ||
| 1456 | |||
| 1457 | // client | ||
| 1458 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 1459 | // reason: dap::StoppedEventReason::Pause, | ||
| 1460 | // description: None, | ||
| 1461 | // thread_id: Some(1), | ||
| 1462 | // preserve_focus_hint: None, | ||
| 1463 | // text: None, | ||
| 1464 | // all_threads_stopped: None, | ||
| 1465 | // hit_breakpoint_ids: None, | ||
| 1466 | // })) | ||
| 1467 | // .await; | ||
| 1468 | |||
| 1469 | // host_cx.run_until_parked(); | ||
| 1470 | // remote_cx.run_until_parked(); | ||
| 1471 | |||
| 1472 | // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { | ||
| 1473 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1474 | // let active_debug_panel_item = debug_panel | ||
| 1475 | // .update(cx, |this, cx| this.active_debug_panel_item(cx)) | ||
| 1476 | // .unwrap(); | ||
| 1477 | |||
| 1478 | // assert_eq!( | ||
| 1479 | // 1, | ||
| 1480 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 1481 | // ); | ||
| 1482 | // assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); | ||
| 1483 | // assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); | ||
| 1484 | // active_debug_panel_item | ||
| 1485 | // }); | ||
| 1486 | |||
| 1487 | // let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 1488 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1489 | // let active_debug_panel_item = debug_panel | ||
| 1490 | // .update(cx, |this, cx| this.active_debug_panel_item(cx)) | ||
| 1491 | // .unwrap(); | ||
| 1492 | |||
| 1493 | // assert_eq!( | ||
| 1494 | // 1, | ||
| 1495 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 1496 | // ); | ||
| 1497 | // assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); | ||
| 1498 | // assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); | ||
| 1499 | // active_debug_panel_item | ||
| 1500 | // }); | ||
| 1501 | |||
| 1502 | // let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"]; | ||
| 1503 | // let first_variable_containers = vec![ | ||
| 1504 | // VariableContainer { | ||
| 1505 | // container_reference: scopes[0].variables_reference, | ||
| 1506 | // variable: variable_1.clone(), | ||
| 1507 | // depth: 1, | ||
| 1508 | // }, | ||
| 1509 | // VariableContainer { | ||
| 1510 | // container_reference: scopes[0].variables_reference, | ||
| 1511 | // variable: variable_2.clone(), | ||
| 1512 | // depth: 1, | ||
| 1513 | // }, | ||
| 1514 | // ]; | ||
| 1515 | |||
| 1516 | // local_debug_item | ||
| 1517 | // .update(host_cx, |this, _| this.variable_list().clone()) | ||
| 1518 | // .update(host_cx, |variable_list, cx| { | ||
| 1519 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1520 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1521 | // assert_eq!(&first_variable_containers, &variable_list.variables()); | ||
| 1522 | |||
| 1523 | // variable_list.assert_visual_entries(first_visual_entries.clone(), cx); | ||
| 1524 | // }); | ||
| 1525 | |||
| 1526 | // client | ||
| 1527 | // .on_request::<Variables, _>({ | ||
| 1528 | // let variables = Arc::new(vec![variable_3.clone()]); | ||
| 1529 | // move |_, args| { | ||
| 1530 | // assert_eq!(2, args.variables_reference); | ||
| 1531 | |||
| 1532 | // Ok(dap::VariablesResponse { | ||
| 1533 | // variables: (*variables).clone(), | ||
| 1534 | // }) | ||
| 1535 | // } | ||
| 1536 | // }) | ||
| 1537 | // .await; | ||
| 1538 | |||
| 1539 | // remote_debug_item | ||
| 1540 | // .update(remote_cx, |this, _| this.variable_list().clone()) | ||
| 1541 | // .update(remote_cx, |variable_list, cx| { | ||
| 1542 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1543 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1544 | // assert_eq!(&first_variable_containers, &variable_list.variables()); | ||
| 1545 | |||
| 1546 | // variable_list.assert_visual_entries(first_visual_entries.clone(), cx); | ||
| 1547 | |||
| 1548 | // variable_list.toggle_variable(&scopes[0], &variable_1, 1, cx); | ||
| 1549 | // }); | ||
| 1550 | |||
| 1551 | // host_cx.run_until_parked(); | ||
| 1552 | // remote_cx.run_until_parked(); | ||
| 1553 | |||
| 1554 | // let second_req_variable_list = vec![ | ||
| 1555 | // VariableContainer { | ||
| 1556 | // container_reference: scopes[0].variables_reference, | ||
| 1557 | // variable: variable_1.clone(), | ||
| 1558 | // depth: 1, | ||
| 1559 | // }, | ||
| 1560 | // VariableContainer { | ||
| 1561 | // container_reference: variable_1.variables_reference, | ||
| 1562 | // variable: variable_3.clone(), | ||
| 1563 | // depth: 2, | ||
| 1564 | // }, | ||
| 1565 | // VariableContainer { | ||
| 1566 | // container_reference: scopes[0].variables_reference, | ||
| 1567 | // variable: variable_2.clone(), | ||
| 1568 | // depth: 1, | ||
| 1569 | // }, | ||
| 1570 | // ]; | ||
| 1571 | |||
| 1572 | // remote_debug_item | ||
| 1573 | // .update(remote_cx, |this, _| this.variable_list().clone()) | ||
| 1574 | // .update(remote_cx, |variable_list, cx| { | ||
| 1575 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1576 | // assert_eq!(3, variable_list.variables().len()); | ||
| 1577 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1578 | // assert_eq!(&second_req_variable_list, &variable_list.variables()); | ||
| 1579 | |||
| 1580 | // variable_list.assert_visual_entries( | ||
| 1581 | // vec![ | ||
| 1582 | // "v Scope 1", | ||
| 1583 | // " v variable 1", | ||
| 1584 | // " > variable 3", | ||
| 1585 | // " > variable 2", | ||
| 1586 | // ], | ||
| 1587 | // cx, | ||
| 1588 | // ); | ||
| 1589 | // }); | ||
| 1590 | |||
| 1591 | // client | ||
| 1592 | // .on_request::<Variables, _>({ | ||
| 1593 | // let variables = Arc::new(vec![variable_4.clone()]); | ||
| 1594 | // move |_, args| { | ||
| 1595 | // assert_eq!(3, args.variables_reference); | ||
| 1596 | |||
| 1597 | // Ok(dap::VariablesResponse { | ||
| 1598 | // variables: (*variables).clone(), | ||
| 1599 | // }) | ||
| 1600 | // } | ||
| 1601 | // }) | ||
| 1602 | // .await; | ||
| 1603 | |||
| 1604 | // local_debug_item | ||
| 1605 | // .update(host_cx, |this, _| this.variable_list().clone()) | ||
| 1606 | // .update(host_cx, |variable_list, cx| { | ||
| 1607 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1608 | // assert_eq!(3, variable_list.variables().len()); | ||
| 1609 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1610 | // assert_eq!(&second_req_variable_list, &variable_list.variables()); | ||
| 1611 | |||
| 1612 | // variable_list.assert_visual_entries(first_visual_entries.clone(), cx); | ||
| 1613 | |||
| 1614 | // variable_list.toggle_variable(&scopes[0], &variable_2.clone(), 1, cx); | ||
| 1615 | // }); | ||
| 1616 | |||
| 1617 | // host_cx.run_until_parked(); | ||
| 1618 | // remote_cx.run_until_parked(); | ||
| 1619 | |||
| 1620 | // let final_variable_containers: Vec<VariableContainer> = vec![ | ||
| 1621 | // VariableContainer { | ||
| 1622 | // container_reference: scopes[0].variables_reference, | ||
| 1623 | // variable: variable_1.clone(), | ||
| 1624 | // depth: 1, | ||
| 1625 | // }, | ||
| 1626 | // VariableContainer { | ||
| 1627 | // container_reference: variable_1.variables_reference, | ||
| 1628 | // variable: variable_3.clone(), | ||
| 1629 | // depth: 2, | ||
| 1630 | // }, | ||
| 1631 | // VariableContainer { | ||
| 1632 | // container_reference: scopes[0].variables_reference, | ||
| 1633 | // variable: variable_2.clone(), | ||
| 1634 | // depth: 1, | ||
| 1635 | // }, | ||
| 1636 | // VariableContainer { | ||
| 1637 | // container_reference: variable_2.variables_reference, | ||
| 1638 | // variable: variable_4.clone(), | ||
| 1639 | // depth: 2, | ||
| 1640 | // }, | ||
| 1641 | // ]; | ||
| 1642 | |||
| 1643 | // remote_debug_item | ||
| 1644 | // .update(remote_cx, |this, _| this.variable_list().clone()) | ||
| 1645 | // .update(remote_cx, |variable_list, cx| { | ||
| 1646 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1647 | // assert_eq!(4, variable_list.variables().len()); | ||
| 1648 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1649 | // assert_eq!(&final_variable_containers, &variable_list.variables()); | ||
| 1650 | |||
| 1651 | // variable_list.assert_visual_entries( | ||
| 1652 | // vec![ | ||
| 1653 | // "v Scope 1", | ||
| 1654 | // " v variable 1", | ||
| 1655 | // " > variable 3", | ||
| 1656 | // " > variable 2", | ||
| 1657 | // ], | ||
| 1658 | // cx, | ||
| 1659 | // ); | ||
| 1660 | // }); | ||
| 1661 | |||
| 1662 | // local_debug_item | ||
| 1663 | // .update(host_cx, |this, _| this.variable_list().clone()) | ||
| 1664 | // .update(host_cx, |variable_list, cx| { | ||
| 1665 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1666 | // assert_eq!(4, variable_list.variables().len()); | ||
| 1667 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1668 | // assert_eq!(&final_variable_containers, &variable_list.variables()); | ||
| 1669 | |||
| 1670 | // variable_list.assert_visual_entries( | ||
| 1671 | // vec![ | ||
| 1672 | // "v Scope 1", | ||
| 1673 | // " > variable 1", | ||
| 1674 | // " v variable 2", | ||
| 1675 | // " > variable 4", | ||
| 1676 | // ], | ||
| 1677 | // cx, | ||
| 1678 | // ); | ||
| 1679 | // }); | ||
| 1680 | |||
| 1681 | // late_join_zed.join_project(host_project_id).await; | ||
| 1682 | // let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = | ||
| 1683 | // late_join_zed.expand().await; | ||
| 1684 | |||
| 1685 | // late_join_cx.run_until_parked(); | ||
| 1686 | |||
| 1687 | // let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { | ||
| 1688 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1689 | // let active_debug_panel_item = debug_panel | ||
| 1690 | // .update(cx, |this, cx| this.active_debug_panel_item(cx)) | ||
| 1691 | // .unwrap(); | ||
| 1692 | |||
| 1693 | // assert_eq!( | ||
| 1694 | // 1, | ||
| 1695 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 1696 | // ); | ||
| 1697 | // assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); | ||
| 1698 | // assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); | ||
| 1699 | // active_debug_panel_item | ||
| 1700 | // }); | ||
| 1701 | |||
| 1702 | // last_join_remote_item | ||
| 1703 | // .update(late_join_cx, |this, _| this.variable_list().clone()) | ||
| 1704 | // .update(late_join_cx, |variable_list, cx| { | ||
| 1705 | // assert_eq!(1, variable_list.scopes().len()); | ||
| 1706 | // assert_eq!(4, variable_list.variables().len()); | ||
| 1707 | // assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); | ||
| 1708 | // assert_eq!(final_variable_containers, variable_list.variables()); | ||
| 1709 | |||
| 1710 | // variable_list.assert_visual_entries(first_visual_entries, cx); | ||
| 1711 | // }); | ||
| 1712 | |||
| 1713 | // let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 1714 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 1715 | // dap_store.shutdown_session(&session.read(cx).id(), cx) | ||
| 1716 | // }) | ||
| 1717 | // }); | ||
| 1718 | |||
| 1719 | // shutdown_client.await.unwrap(); | ||
| 1720 | // } | ||
| 1721 | |||
| 1722 | #[gpui::test] | ||
| 1723 | async fn test_ignore_breakpoints( | ||
| 1724 | _host_cx: &mut TestAppContext, | ||
| 1725 | _remote_cx: &mut TestAppContext, | ||
| 1726 | _cx_c: &mut TestAppContext, | ||
| 1727 | ) { | ||
| 1728 | unimplemented!("Collab is still being refactored"); | ||
| 1729 | // let executor = host_cx.executor(); | ||
| 1730 | // let mut server = TestServer::start(executor).await; | ||
| 1731 | |||
| 1732 | // let (mut host_zed, mut remote_zed, mut late_join_zed) = | ||
| 1733 | // setup_three_member_test(&mut server, host_cx, remote_cx, cx_c).await; | ||
| 1734 | |||
| 1735 | // let (host_project_id, worktree_id) = host_zed | ||
| 1736 | // .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) | ||
| 1737 | // .await; | ||
| 1738 | |||
| 1739 | // remote_zed.join_project(host_project_id).await; | ||
| 1740 | |||
| 1741 | // let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 1742 | // let (_client_remote, remote_workspace, remote_project, remote_cx) = remote_zed.expand().await; | ||
| 1743 | |||
| 1744 | // let project_path = ProjectPath { | ||
| 1745 | // worktree_id, | ||
| 1746 | // path: Arc::from(Path::new(&"test.txt")), | ||
| 1747 | // }; | ||
| 1748 | |||
| 1749 | // let local_editor = host_workspace | ||
| 1750 | // .update_in(host_cx, |workspace, window, cx| { | ||
| 1751 | // workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 1752 | // }) | ||
| 1753 | // .await | ||
| 1754 | // .unwrap() | ||
| 1755 | // .downcast::<Editor>() | ||
| 1756 | // .unwrap(); | ||
| 1757 | |||
| 1758 | // local_editor.update_in(host_cx, |editor, window, cx| { | ||
| 1759 | // editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 1760 | // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); // Line 2 | ||
| 1761 | // editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 1762 | // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 1763 | // // Line 3 | ||
| 1764 | // }); | ||
| 1765 | |||
| 1766 | // host_cx.run_until_parked(); | ||
| 1767 | // remote_cx.run_until_parked(); | ||
| 1768 | |||
| 1769 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 1770 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 1771 | // }); | ||
| 1772 | |||
| 1773 | // let session = task.await.unwrap(); | ||
| 1774 | // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 1775 | // let client_id = client.id(); | ||
| 1776 | |||
| 1777 | // client | ||
| 1778 | // .on_request::<Initialize, _>(move |_, _| { | ||
| 1779 | // Ok(dap::Capabilities { | ||
| 1780 | // supports_configuration_done_request: Some(true), | ||
| 1781 | // ..Default::default() | ||
| 1782 | // }) | ||
| 1783 | // }) | ||
| 1784 | // .await; | ||
| 1785 | |||
| 1786 | // let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 1787 | // client | ||
| 1788 | // .on_request::<SetBreakpoints, _>({ | ||
| 1789 | // let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 1790 | // move |_, args| { | ||
| 1791 | // assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 1792 | |||
| 1793 | // let mut actual_breakpoints = args.breakpoints.unwrap(); | ||
| 1794 | // actual_breakpoints.sort_by_key(|b| b.line); | ||
| 1795 | |||
| 1796 | // let expected_breakpoints = vec![ | ||
| 1797 | // SourceBreakpoint { | ||
| 1798 | // line: 2, | ||
| 1799 | // column: None, | ||
| 1800 | // condition: None, | ||
| 1801 | // hit_condition: None, | ||
| 1802 | // log_message: None, | ||
| 1803 | // mode: None, | ||
| 1804 | // }, | ||
| 1805 | // SourceBreakpoint { | ||
| 1806 | // line: 3, | ||
| 1807 | // column: None, | ||
| 1808 | // condition: None, | ||
| 1809 | // hit_condition: None, | ||
| 1810 | // log_message: None, | ||
| 1811 | // mode: None, | ||
| 1812 | // }, | ||
| 1813 | // ]; | ||
| 1814 | |||
| 1815 | // assert_eq!(actual_breakpoints, expected_breakpoints); | ||
| 1816 | |||
| 1817 | // called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 1818 | |||
| 1819 | // Ok(dap::SetBreakpointsResponse { | ||
| 1820 | // breakpoints: Vec::default(), | ||
| 1821 | // }) | ||
| 1822 | // } | ||
| 1823 | // }) | ||
| 1824 | // .await; | ||
| 1825 | |||
| 1826 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 1827 | // client | ||
| 1828 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 1829 | // Ok(dap::StackTraceResponse { | ||
| 1830 | // stack_frames: Vec::default(), | ||
| 1831 | // total_frames: None, | ||
| 1832 | // }) | ||
| 1833 | // }) | ||
| 1834 | // .await; | ||
| 1835 | |||
| 1836 | // client | ||
| 1837 | // .fake_event(dap::messages::Events::Initialized(Some( | ||
| 1838 | // dap::Capabilities { | ||
| 1839 | // supports_configuration_done_request: Some(true), | ||
| 1840 | // ..Default::default() | ||
| 1841 | // }, | ||
| 1842 | // ))) | ||
| 1843 | // .await; | ||
| 1844 | |||
| 1845 | // host_cx.run_until_parked(); | ||
| 1846 | // remote_cx.run_until_parked(); | ||
| 1847 | |||
| 1848 | // assert!( | ||
| 1849 | // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1850 | // "SetBreakpoint request must be called when starting debug session" | ||
| 1851 | // ); | ||
| 1852 | |||
| 1853 | // client | ||
| 1854 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 1855 | // reason: dap::StoppedEventReason::Pause, | ||
| 1856 | // description: None, | ||
| 1857 | // thread_id: Some(1), | ||
| 1858 | // preserve_focus_hint: None, | ||
| 1859 | // text: None, | ||
| 1860 | // all_threads_stopped: None, | ||
| 1861 | // hit_breakpoint_ids: None, | ||
| 1862 | // })) | ||
| 1863 | // .await; | ||
| 1864 | |||
| 1865 | // host_cx.run_until_parked(); | ||
| 1866 | // remote_cx.run_until_parked(); | ||
| 1867 | |||
| 1868 | // let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { | ||
| 1869 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1870 | // let active_session = debug_panel | ||
| 1871 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 1872 | // .unwrap(); | ||
| 1873 | |||
| 1874 | // assert_eq!( | ||
| 1875 | // 1, | ||
| 1876 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 1877 | // ); | ||
| 1878 | |||
| 1879 | // let session_id = debug_panel.update(cx, |this, cx| { | ||
| 1880 | // this.dap_store() | ||
| 1881 | // .read(cx) | ||
| 1882 | // .session_by_client_id(client.id()) | ||
| 1883 | // .unwrap() | ||
| 1884 | // .read(cx) | ||
| 1885 | // .id() | ||
| 1886 | // }); | ||
| 1887 | |||
| 1888 | // let breakpoints_ignored = active_session.read(cx).are_breakpoints_ignored(cx); | ||
| 1889 | |||
| 1890 | // assert_eq!(session_id, active_session.read(cx).session().read(cx).id()); | ||
| 1891 | // assert_eq!(false, breakpoints_ignored); | ||
| 1892 | // assert_eq!(client.id(), active_session.read(cx).client_id()); | ||
| 1893 | // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 1894 | // active_session | ||
| 1895 | // }); | ||
| 1896 | |||
| 1897 | // called_set_breakpoints.store(false, Ordering::SeqCst); | ||
| 1898 | |||
| 1899 | // client | ||
| 1900 | // .on_request::<SetBreakpoints, _>({ | ||
| 1901 | // let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 1902 | // move |_, args| { | ||
| 1903 | // assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 1904 | // assert_eq!(args.breakpoints, Some(vec![])); | ||
| 1905 | |||
| 1906 | // called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 1907 | |||
| 1908 | // Ok(dap::SetBreakpointsResponse { | ||
| 1909 | // breakpoints: Vec::default(), | ||
| 1910 | // }) | ||
| 1911 | // } | ||
| 1912 | // }) | ||
| 1913 | // .await; | ||
| 1914 | |||
| 1915 | // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { | ||
| 1916 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 1917 | // let active_session = debug_panel | ||
| 1918 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 1919 | // .unwrap(); | ||
| 1920 | |||
| 1921 | // assert_eq!( | ||
| 1922 | // 1, | ||
| 1923 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 1924 | // ); | ||
| 1925 | |||
| 1926 | // assert_eq!(false, active_session.read(cx).are_breakpoints_ignored(cx)); | ||
| 1927 | // assert_eq!(client.id(), active_session.read(cx).client_id()); | ||
| 1928 | // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 1929 | |||
| 1930 | // active_session | ||
| 1931 | // }); | ||
| 1932 | |||
| 1933 | // local_debug_item.update(host_cx, |item, cx| { | ||
| 1934 | // item.toggle_ignore_breakpoints(cx); // Set to true | ||
| 1935 | // assert_eq!(true, item.are_breakpoints_ignored(cx)); | ||
| 1936 | // }); | ||
| 1937 | |||
| 1938 | // host_cx.run_until_parked(); | ||
| 1939 | // remote_cx.run_until_parked(); | ||
| 1940 | |||
| 1941 | // assert!( | ||
| 1942 | // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1943 | // "SetBreakpoint request must be called to ignore breakpoints" | ||
| 1944 | // ); | ||
| 1945 | |||
| 1946 | // client | ||
| 1947 | // .on_request::<SetBreakpoints, _>({ | ||
| 1948 | // let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 1949 | // move |_, _args| { | ||
| 1950 | // called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 1951 | |||
| 1952 | // Ok(dap::SetBreakpointsResponse { | ||
| 1953 | // breakpoints: Vec::default(), | ||
| 1954 | // }) | ||
| 1955 | // } | ||
| 1956 | // }) | ||
| 1957 | // .await; | ||
| 1958 | |||
| 1959 | // let remote_editor = remote_workspace | ||
| 1960 | // .update_in(remote_cx, |workspace, window, cx| { | ||
| 1961 | // workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 1962 | // }) | ||
| 1963 | // .await | ||
| 1964 | // .unwrap() | ||
| 1965 | // .downcast::<Editor>() | ||
| 1966 | // .unwrap(); | ||
| 1967 | |||
| 1968 | // called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst); | ||
| 1969 | |||
| 1970 | // remote_editor.update_in(remote_cx, |editor, window, cx| { | ||
| 1971 | // // Line 1 | ||
| 1972 | // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 1973 | // }); | ||
| 1974 | |||
| 1975 | // host_cx.run_until_parked(); | ||
| 1976 | // remote_cx.run_until_parked(); | ||
| 1977 | |||
| 1978 | // assert!( | ||
| 1979 | // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1980 | // "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints" | ||
| 1981 | // ); | ||
| 1982 | |||
| 1983 | // remote_debug_item.update(remote_cx, |debug_panel, cx| { | ||
| 1984 | // let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx); | ||
| 1985 | |||
| 1986 | // assert_eq!(true, breakpoints_ignored); | ||
| 1987 | // assert_eq!(client.id(), debug_panel.client_id()); | ||
| 1988 | // assert_eq!(1, debug_panel.thread_id().0); | ||
| 1989 | // }); | ||
| 1990 | |||
| 1991 | // client | ||
| 1992 | // .on_request::<SetBreakpoints, _>({ | ||
| 1993 | // let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 1994 | // move |_, args| { | ||
| 1995 | // assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 1996 | |||
| 1997 | // let mut actual_breakpoints = args.breakpoints.unwrap(); | ||
| 1998 | // actual_breakpoints.sort_by_key(|b| b.line); | ||
| 1999 | |||
| 2000 | // let expected_breakpoints = vec![ | ||
| 2001 | // SourceBreakpoint { | ||
| 2002 | // line: 1, | ||
| 2003 | // column: None, | ||
| 2004 | // condition: None, | ||
| 2005 | // hit_condition: None, | ||
| 2006 | // log_message: None, | ||
| 2007 | // mode: None, | ||
| 2008 | // }, | ||
| 2009 | // SourceBreakpoint { | ||
| 2010 | // line: 2, | ||
| 2011 | // column: None, | ||
| 2012 | // condition: None, | ||
| 2013 | // hit_condition: None, | ||
| 2014 | // log_message: None, | ||
| 2015 | // mode: None, | ||
| 2016 | // }, | ||
| 2017 | // SourceBreakpoint { | ||
| 2018 | // line: 3, | ||
| 2019 | // column: None, | ||
| 2020 | // condition: None, | ||
| 2021 | // hit_condition: None, | ||
| 2022 | // log_message: None, | ||
| 2023 | // mode: None, | ||
| 2024 | // }, | ||
| 2025 | // ]; | ||
| 2026 | |||
| 2027 | // assert_eq!(actual_breakpoints, expected_breakpoints); | ||
| 2028 | |||
| 2029 | // called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 2030 | |||
| 2031 | // Ok(dap::SetBreakpointsResponse { | ||
| 2032 | // breakpoints: Vec::default(), | ||
| 2033 | // }) | ||
| 2034 | // } | ||
| 2035 | // }) | ||
| 2036 | // .await; | ||
| 2037 | |||
| 2038 | // late_join_zed.join_project(host_project_id).await; | ||
| 2039 | // let (_late_join_client, late_join_workspace, late_join_project, late_join_cx) = | ||
| 2040 | // late_join_zed.expand().await; | ||
| 2041 | |||
| 2042 | // late_join_cx.run_until_parked(); | ||
| 2043 | |||
| 2044 | // let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { | ||
| 2045 | // let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 2046 | // let active_session = debug_panel | ||
| 2047 | // .update(cx, |this, cx| this.active_session(cx)) | ||
| 2048 | // .unwrap(); | ||
| 2049 | |||
| 2050 | // let breakpoints_ignored = active_session.read(cx).are_breakpoints_ignored(cx); | ||
| 2051 | |||
| 2052 | // assert_eq!(true, breakpoints_ignored); | ||
| 2053 | |||
| 2054 | // assert_eq!( | ||
| 2055 | // 1, | ||
| 2056 | // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 2057 | // ); | ||
| 2058 | // assert_eq!(client.id(), active_session.read(cx).client_id()); | ||
| 2059 | // assert_eq!(1, active_session.read(cx).thread_id().0); | ||
| 2060 | // active_session | ||
| 2061 | // }); | ||
| 2062 | |||
| 2063 | // remote_debug_item.update(remote_cx, |item, cx| { | ||
| 2064 | // item.toggle_ignore_breakpoints(cx); | ||
| 2065 | // }); | ||
| 2066 | |||
| 2067 | // host_cx.run_until_parked(); | ||
| 2068 | // remote_cx.run_until_parked(); | ||
| 2069 | // late_join_cx.run_until_parked(); | ||
| 2070 | |||
| 2071 | // assert!( | ||
| 2072 | // called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 2073 | // "SetBreakpoint request should be called to update breakpoints" | ||
| 2074 | // ); | ||
| 2075 | |||
| 2076 | // client | ||
| 2077 | // .on_request::<SetBreakpoints, _>({ | ||
| 2078 | // let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 2079 | // move |_, args| { | ||
| 2080 | // assert_eq!("/project/test.txt", args.source.path.unwrap()); | ||
| 2081 | // assert_eq!(args.breakpoints, Some(vec![])); | ||
| 2082 | |||
| 2083 | // called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 2084 | |||
| 2085 | // Ok(dap::SetBreakpointsResponse { | ||
| 2086 | // breakpoints: Vec::default(), | ||
| 2087 | // }) | ||
| 2088 | // } | ||
| 2089 | // }) | ||
| 2090 | // .await; | ||
| 2091 | |||
| 2092 | // local_debug_item.update(host_cx, |debug_panel_item, cx| { | ||
| 2093 | // assert_eq!( | ||
| 2094 | // false, | ||
| 2095 | // debug_panel_item.are_breakpoints_ignored(cx), | ||
| 2096 | // "Remote client set this to false" | ||
| 2097 | // ); | ||
| 2098 | // }); | ||
| 2099 | |||
| 2100 | // remote_debug_item.update(remote_cx, |debug_panel_item, cx| { | ||
| 2101 | // assert_eq!( | ||
| 2102 | // false, | ||
| 2103 | // debug_panel_item.are_breakpoints_ignored(cx), | ||
| 2104 | // "Remote client set this to false" | ||
| 2105 | // ); | ||
| 2106 | // }); | ||
| 2107 | |||
| 2108 | // last_join_remote_item.update(late_join_cx, |debug_panel_item, cx| { | ||
| 2109 | // assert_eq!( | ||
| 2110 | // false, | ||
| 2111 | // debug_panel_item.are_breakpoints_ignored(cx), | ||
| 2112 | // "Remote client set this to false" | ||
| 2113 | // ); | ||
| 2114 | // }); | ||
| 2115 | |||
| 2116 | // let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 2117 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 2118 | // dap_store.shutdown_session(&session.read(cx).id(), cx) | ||
| 2119 | // }) | ||
| 2120 | // }); | ||
| 2121 | |||
| 2122 | // shutdown_client.await.unwrap(); | ||
| 2123 | |||
| 2124 | // host_cx.run_until_parked(); | ||
| 2125 | // remote_cx.run_until_parked(); | ||
| 2126 | |||
| 2127 | // remote_project.update(remote_cx, |project, cx| { | ||
| 2128 | // project.dap_store().update(cx, |dap_store, _cx| { | ||
| 2129 | // let sessions = dap_store.sessions().collect::<Vec<_>>(); | ||
| 2130 | |||
| 2131 | // assert_eq!( | ||
| 2132 | // None, | ||
| 2133 | // dap_store.session_by_client_id(&client_id), | ||
| 2134 | // "No client_id to session mapping should exist after shutdown" | ||
| 2135 | // ); | ||
| 2136 | // assert_eq!( | ||
| 2137 | // 0, | ||
| 2138 | // sessions.len(), | ||
| 2139 | // "No sessions should be left after shutdown" | ||
| 2140 | // ); | ||
| 2141 | // }) | ||
| 2142 | // }); | ||
| 2143 | |||
| 2144 | // late_join_project.update(late_join_cx, |project, cx| { | ||
| 2145 | // project.dap_store().update(cx, |dap_store, _cx| { | ||
| 2146 | // let sessions = dap_store.sessions().collect::<Vec<_>>(); | ||
| 2147 | |||
| 2148 | // assert_eq!( | ||
| 2149 | // None, | ||
| 2150 | // dap_store.session_by_client_id(&client_id), | ||
| 2151 | // "No client_id to session mapping should exist after shutdown" | ||
| 2152 | // ); | ||
| 2153 | // assert_eq!( | ||
| 2154 | // 0, | ||
| 2155 | // sessions.len(), | ||
| 2156 | // "No sessions should be left after shutdown" | ||
| 2157 | // ); | ||
| 2158 | // }) | ||
| 2159 | // }); | ||
| 2160 | } | ||
| 2161 | |||
| 2162 | #[gpui::test] | ||
| 2163 | async fn test_debug_panel_console(_host_cx: &mut TestAppContext, _remote_cx: &mut TestAppContext) { | ||
| 2164 | unimplemented!("Collab is still being refactored"); | ||
| 2165 | // let executor = host_cx.executor(); | ||
| 2166 | // let mut server = TestServer::start(executor).await; | ||
| 2167 | |||
| 2168 | // let (mut host_zed, mut remote_zed) = | ||
| 2169 | // setup_two_member_test(&mut server, host_cx, remote_cx).await; | ||
| 2170 | |||
| 2171 | // let (host_project_id, _) = host_zed.host_project(None).await; | ||
| 2172 | // remote_zed.join_project(host_project_id).await; | ||
| 2173 | |||
| 2174 | // let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; | ||
| 2175 | // let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; | ||
| 2176 | |||
| 2177 | // remote_cx.run_until_parked(); | ||
| 2178 | |||
| 2179 | // let task = host_project.update(host_cx, |project, cx| { | ||
| 2180 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 2181 | // }); | ||
| 2182 | |||
| 2183 | // let session = task.await.unwrap(); | ||
| 2184 | // let client = session.read_with(host_cx, |project, _| project.adapter_client().unwrap()); | ||
| 2185 | |||
| 2186 | // client | ||
| 2187 | // .on_request::<Initialize, _>(move |_, _| { | ||
| 2188 | // Ok(dap::Capabilities { | ||
| 2189 | // supports_step_back: Some(false), | ||
| 2190 | // ..Default::default() | ||
| 2191 | // }) | ||
| 2192 | // }) | ||
| 2193 | // .await; | ||
| 2194 | |||
| 2195 | // client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 2196 | |||
| 2197 | // client | ||
| 2198 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 2199 | // Ok(dap::StackTraceResponse { | ||
| 2200 | // stack_frames: Vec::default(), | ||
| 2201 | // total_frames: None, | ||
| 2202 | // }) | ||
| 2203 | // }) | ||
| 2204 | // .await; | ||
| 2205 | |||
| 2206 | // client | ||
| 2207 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 2208 | // reason: dap::StoppedEventReason::Pause, | ||
| 2209 | // description: None, | ||
| 2210 | // thread_id: Some(1), | ||
| 2211 | // preserve_focus_hint: None, | ||
| 2212 | // text: None, | ||
| 2213 | // all_threads_stopped: None, | ||
| 2214 | // hit_breakpoint_ids: None, | ||
| 2215 | // })) | ||
| 2216 | // .await; | ||
| 2217 | |||
| 2218 | // client | ||
| 2219 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2220 | // category: None, | ||
| 2221 | // output: "First line".to_string(), | ||
| 2222 | // data: None, | ||
| 2223 | // variables_reference: None, | ||
| 2224 | // source: None, | ||
| 2225 | // line: None, | ||
| 2226 | // column: None, | ||
| 2227 | // group: None, | ||
| 2228 | // location_reference: None, | ||
| 2229 | // })) | ||
| 2230 | // .await; | ||
| 2231 | |||
| 2232 | // client | ||
| 2233 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2234 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2235 | // output: "First group".to_string(), | ||
| 2236 | // data: None, | ||
| 2237 | // variables_reference: None, | ||
| 2238 | // source: None, | ||
| 2239 | // line: None, | ||
| 2240 | // column: None, | ||
| 2241 | // group: Some(dap::OutputEventGroup::Start), | ||
| 2242 | // location_reference: None, | ||
| 2243 | // })) | ||
| 2244 | // .await; | ||
| 2245 | |||
| 2246 | // client | ||
| 2247 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2248 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2249 | // output: "First item in group 1".to_string(), | ||
| 2250 | // data: None, | ||
| 2251 | // variables_reference: None, | ||
| 2252 | // source: None, | ||
| 2253 | // line: None, | ||
| 2254 | // column: None, | ||
| 2255 | // group: None, | ||
| 2256 | // location_reference: None, | ||
| 2257 | // })) | ||
| 2258 | // .await; | ||
| 2259 | |||
| 2260 | // client | ||
| 2261 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2262 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2263 | // output: "Second item in group 1".to_string(), | ||
| 2264 | // data: None, | ||
| 2265 | // variables_reference: None, | ||
| 2266 | // source: None, | ||
| 2267 | // line: None, | ||
| 2268 | // column: None, | ||
| 2269 | // group: None, | ||
| 2270 | // location_reference: None, | ||
| 2271 | // })) | ||
| 2272 | // .await; | ||
| 2273 | |||
| 2274 | // client | ||
| 2275 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2276 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2277 | // output: "Second group".to_string(), | ||
| 2278 | // data: None, | ||
| 2279 | // variables_reference: None, | ||
| 2280 | // source: None, | ||
| 2281 | // line: None, | ||
| 2282 | // column: None, | ||
| 2283 | // group: Some(dap::OutputEventGroup::Start), | ||
| 2284 | // location_reference: None, | ||
| 2285 | // })) | ||
| 2286 | // .await; | ||
| 2287 | |||
| 2288 | // client | ||
| 2289 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2290 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2291 | // output: "First item in group 2".to_string(), | ||
| 2292 | // data: None, | ||
| 2293 | // variables_reference: None, | ||
| 2294 | // source: None, | ||
| 2295 | // line: None, | ||
| 2296 | // column: None, | ||
| 2297 | // group: None, | ||
| 2298 | // location_reference: None, | ||
| 2299 | // })) | ||
| 2300 | // .await; | ||
| 2301 | |||
| 2302 | // client | ||
| 2303 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2304 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2305 | // output: "Second item in group 2".to_string(), | ||
| 2306 | // data: None, | ||
| 2307 | // variables_reference: None, | ||
| 2308 | // source: None, | ||
| 2309 | // line: None, | ||
| 2310 | // column: None, | ||
| 2311 | // group: None, | ||
| 2312 | // location_reference: None, | ||
| 2313 | // })) | ||
| 2314 | // .await; | ||
| 2315 | |||
| 2316 | // client | ||
| 2317 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2318 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2319 | // output: "End group 2".to_string(), | ||
| 2320 | // data: None, | ||
| 2321 | // variables_reference: None, | ||
| 2322 | // source: None, | ||
| 2323 | // line: None, | ||
| 2324 | // column: None, | ||
| 2325 | // group: Some(dap::OutputEventGroup::End), | ||
| 2326 | // location_reference: None, | ||
| 2327 | // })) | ||
| 2328 | // .await; | ||
| 2329 | |||
| 2330 | // client | ||
| 2331 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2332 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2333 | // output: "Third group".to_string(), | ||
| 2334 | // data: None, | ||
| 2335 | // variables_reference: None, | ||
| 2336 | // source: None, | ||
| 2337 | // line: None, | ||
| 2338 | // column: None, | ||
| 2339 | // group: Some(dap::OutputEventGroup::StartCollapsed), | ||
| 2340 | // location_reference: None, | ||
| 2341 | // })) | ||
| 2342 | // .await; | ||
| 2343 | |||
| 2344 | // client | ||
| 2345 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2346 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2347 | // output: "First item in group 3".to_string(), | ||
| 2348 | // data: None, | ||
| 2349 | // variables_reference: None, | ||
| 2350 | // source: None, | ||
| 2351 | // line: None, | ||
| 2352 | // column: None, | ||
| 2353 | // group: None, | ||
| 2354 | // location_reference: None, | ||
| 2355 | // })) | ||
| 2356 | // .await; | ||
| 2357 | |||
| 2358 | // client | ||
| 2359 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2360 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2361 | // output: "Second item in group 3".to_string(), | ||
| 2362 | // data: None, | ||
| 2363 | // variables_reference: None, | ||
| 2364 | // source: None, | ||
| 2365 | // line: None, | ||
| 2366 | // column: None, | ||
| 2367 | // group: None, | ||
| 2368 | // location_reference: None, | ||
| 2369 | // })) | ||
| 2370 | // .await; | ||
| 2371 | |||
| 2372 | // client | ||
| 2373 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2374 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2375 | // output: "End group 3".to_string(), | ||
| 2376 | // data: None, | ||
| 2377 | // variables_reference: None, | ||
| 2378 | // source: None, | ||
| 2379 | // line: None, | ||
| 2380 | // column: None, | ||
| 2381 | // group: Some(dap::OutputEventGroup::End), | ||
| 2382 | // location_reference: None, | ||
| 2383 | // })) | ||
| 2384 | // .await; | ||
| 2385 | |||
| 2386 | // client | ||
| 2387 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2388 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2389 | // output: "Third item in group 1".to_string(), | ||
| 2390 | // data: None, | ||
| 2391 | // variables_reference: None, | ||
| 2392 | // source: None, | ||
| 2393 | // line: None, | ||
| 2394 | // column: None, | ||
| 2395 | // group: None, | ||
| 2396 | // location_reference: None, | ||
| 2397 | // })) | ||
| 2398 | // .await; | ||
| 2399 | |||
| 2400 | // client | ||
| 2401 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 2402 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 2403 | // output: "Second item".to_string(), | ||
| 2404 | // data: None, | ||
| 2405 | // variables_reference: None, | ||
| 2406 | // source: None, | ||
| 2407 | // line: None, | ||
| 2408 | // column: None, | ||
| 2409 | // group: Some(dap::OutputEventGroup::End), | ||
| 2410 | // location_reference: None, | ||
| 2411 | // })) | ||
| 2412 | // .await; | ||
| 2413 | |||
| 2414 | // host_cx.run_until_parked(); | ||
| 2415 | // remote_cx.run_until_parked(); | ||
| 2416 | |||
| 2417 | // active_session(remote_workspace, remote_cx).update(remote_cx, |session_item, cx| { | ||
| 2418 | // session_item | ||
| 2419 | // .mode() | ||
| 2420 | // .as_running() | ||
| 2421 | // .unwrap() | ||
| 2422 | // .read(cx) | ||
| 2423 | // .console() | ||
| 2424 | // .update(cx, |console, cx| { | ||
| 2425 | // console.editor().update(cx, |editor, cx| { | ||
| 2426 | // pretty_assertions::assert_eq!( | ||
| 2427 | // " | ||
| 2428 | // <First line | ||
| 2429 | // First group | ||
| 2430 | // First item in group 1 | ||
| 2431 | // Second item in group 1 | ||
| 2432 | // Second group | ||
| 2433 | // First item in group 2 | ||
| 2434 | // Second item in group 2 | ||
| 2435 | // End group 2 | ||
| 2436 | // ⋯ End group 3 | ||
| 2437 | // Third item in group 1 | ||
| 2438 | // Second item | ||
| 2439 | // " | ||
| 2440 | // .unindent(), | ||
| 2441 | // editor.display_text(cx) | ||
| 2442 | // ); | ||
| 2443 | // }) | ||
| 2444 | // }); | ||
| 2445 | // }); | ||
| 2446 | |||
| 2447 | // let shutdown_client = host_project.update(host_cx, |project, cx| { | ||
| 2448 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 2449 | // dap_store.shutdown_session(&session.read(cx).session_id(), cx) | ||
| 2450 | // }) | ||
| 2451 | // }); | ||
| 2452 | |||
| 2453 | // shutdown_client.await.unwrap(); | ||
| 2454 | } | ||
| @@ -21,7 +21,7 @@ use language::{ · 20 unmodified lines | |||
| 21 | }; | 21 | }; |
| 22 | use project::{ | 22 | use project::{ |
| 23 | project_settings::{InlineBlameSettings, ProjectSettings}, | 23 | project_settings::{InlineBlameSettings, ProjectSettings}, |
| 24 | SERVER_PROGRESS_THROTTLE_TIMEOUT, | 24 | ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, |
| 25 | }; | 25 | }; |
| 26 | use recent_projects::disconnected_overlay::DisconnectedOverlay; | 26 | use recent_projects::disconnected_overlay::DisconnectedOverlay; |
| 27 | use rpc::RECEIVE_TIMEOUT; | 27 | use 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] | ||
| 2412 | async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { | ||
| 2413 | let executor = cx_a.executor(); | ||
| 2414 | let mut server = TestServer::start(executor.clone()).await; | ||
| 2415 | let client_a = server.create_client(cx_a, "user_a").await; | ||
| 2416 | let client_b = server.create_client(cx_b, "user_b").await; | ||
| 2417 | server | ||
| 2418 | .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) | ||
| 2419 | .await; | ||
| 2420 | let active_call_a = cx_a.read(ActiveCall::global); | ||
| 2421 | let active_call_b = cx_b.read(ActiveCall::global); | ||
| 2422 | cx_a.update(editor::init); | ||
| 2423 | cx_b.update(editor::init); | ||
| 2424 | client_a | ||
| 2425 | .fs() | ||
| 2426 | .insert_tree( | ||
| 2427 | "/a", | ||
| 2428 | json!({ | ||
| 2429 | "test.txt": "one\ntwo\nthree\nfour\nfive", | ||
| 2430 | }), | ||
| 2431 | ) | ||
| 2432 | .await; | ||
| 2433 | let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; | ||
| 2434 | let project_path = ProjectPath { | ||
| 2435 | worktree_id, | ||
| 2436 | path: Arc::from(Path::new(&"test.txt")), | ||
| 2437 | }; | ||
| 2438 | let abs_path = project_a.read_with(cx_a, |project, cx| { | ||
| 2439 | project | ||
| 2440 | .absolute_path(&project_path, cx) | ||
| 2441 | .map(|path_buf| Arc::from(path_buf.to_owned())) | ||
| 2442 | .unwrap() | ||
| 2443 | }); | ||
| 2444 | |||
| 2445 | active_call_a | ||
| 2446 | .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) | ||
| 2447 | .await | ||
| 2448 | .unwrap(); | ||
| 2449 | let project_id = active_call_a | ||
| 2450 | .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) | ||
| 2451 | .await | ||
| 2452 | .unwrap(); | ||
| 2453 | let project_b = client_b.join_remote_project(project_id, cx_b).await; | ||
| 2454 | active_call_b | ||
| 2455 | .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) | ||
| 2456 | .await | ||
| 2457 | .unwrap(); | ||
| 2458 | let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); | ||
| 2459 | let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); | ||
| 2460 | |||
| 2461 | // Client A opens an editor. | ||
| 2462 | let editor_a = workspace_a | ||
| 2463 | .update_in(cx_a, |workspace, window, cx| { | ||
| 2464 | workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 2465 | }) | ||
| 2466 | .await | ||
| 2467 | .unwrap() | ||
| 2468 | .downcast::<Editor>() | ||
| 2469 | .unwrap(); | ||
| 2470 | |||
| 2471 | // Client B opens same editor as A. | ||
| 2472 | let editor_b = workspace_b | ||
| 2473 | .update_in(cx_b, |workspace, window, cx| { | ||
| 2474 | workspace.open_path(project_path.clone(), None, true, window, cx) | ||
| 2475 | }) | ||
| 2476 | .await | ||
| 2477 | .unwrap() | ||
| 2478 | .downcast::<Editor>() | ||
| 2479 | .unwrap(); | ||
| 2480 | |||
| 2481 | cx_a.run_until_parked(); | ||
| 2482 | cx_b.run_until_parked(); | ||
| 2483 | |||
| 2484 | // Client A adds breakpoint on line (1) | ||
| 2485 | editor_a.update_in(cx_a, |editor, window, cx| { | ||
| 2486 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 2487 | }); | ||
| 2488 | |||
| 2489 | cx_a.run_until_parked(); | ||
| 2490 | cx_b.run_until_parked(); | ||
| 2491 | |||
| 2492 | let breakpoints_a = editor_a.update(cx_a, |editor, cx| { | ||
| 2493 | editor | ||
| 2494 | .breakpoint_store() | ||
| 2495 | .clone() | ||
| 2496 | .unwrap() | ||
| 2497 | .read(cx) | ||
| 2498 | .all_breakpoints(cx) | ||
| 2499 | .clone() | ||
| 2500 | }); | ||
| 2501 | let breakpoints_b = editor_b.update(cx_b, |editor, cx| { | ||
| 2502 | editor | ||
| 2503 | .breakpoint_store() | ||
| 2504 | .clone() | ||
| 2505 | .unwrap() | ||
| 2506 | .read(cx) | ||
| 2507 | .all_breakpoints(cx) | ||
| 2508 | .clone() | ||
| 2509 | }); | ||
| 2510 | |||
| 2511 | assert_eq!(1, breakpoints_a.len()); | ||
| 2512 | assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len()); | ||
| 2513 | assert_eq!(breakpoints_a, breakpoints_b); | ||
| 2514 | |||
| 2515 | // Client B adds breakpoint on line(2) | ||
| 2516 | editor_b.update_in(cx_b, |editor, window, cx| { | ||
| 2517 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 2518 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 2519 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 2520 | }); | ||
| 2521 | |||
| 2522 | cx_a.run_until_parked(); | ||
| 2523 | cx_b.run_until_parked(); | ||
| 2524 | |||
| 2525 | let breakpoints_a = editor_a.update(cx_a, |editor, cx| { | ||
| 2526 | editor | ||
| 2527 | .breakpoint_store() | ||
| 2528 | .clone() | ||
| 2529 | .unwrap() | ||
| 2530 | .read(cx) | ||
| 2531 | .all_breakpoints(cx) | ||
| 2532 | .clone() | ||
| 2533 | }); | ||
| 2534 | let breakpoints_b = editor_b.update(cx_b, |editor, cx| { | ||
| 2535 | editor | ||
| 2536 | .breakpoint_store() | ||
| 2537 | .clone() | ||
| 2538 | .unwrap() | ||
| 2539 | .read(cx) | ||
| 2540 | .all_breakpoints(cx) | ||
| 2541 | .clone() | ||
| 2542 | }); | ||
| 2543 | |||
| 2544 | assert_eq!(1, breakpoints_a.len()); | ||
| 2545 | assert_eq!(breakpoints_a, breakpoints_b); | ||
| 2546 | assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len()); | ||
| 2547 | |||
| 2548 | // Client A removes last added breakpoint from client B | ||
| 2549 | editor_a.update_in(cx_a, |editor, window, cx| { | ||
| 2550 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 2551 | editor.move_down(&editor::actions::MoveDown, window, cx); | ||
| 2552 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 2553 | }); | ||
| 2554 | |||
| 2555 | cx_a.run_until_parked(); | ||
| 2556 | cx_b.run_until_parked(); | ||
| 2557 | |||
| 2558 | let breakpoints_a = editor_a.update(cx_a, |editor, cx| { | ||
| 2559 | editor | ||
| 2560 | .breakpoint_store() | ||
| 2561 | .clone() | ||
| 2562 | .unwrap() | ||
| 2563 | .read(cx) | ||
| 2564 | .all_breakpoints(cx) | ||
| 2565 | .clone() | ||
| 2566 | }); | ||
| 2567 | let breakpoints_b = editor_b.update(cx_b, |editor, cx| { | ||
| 2568 | editor | ||
| 2569 | .breakpoint_store() | ||
| 2570 | .clone() | ||
| 2571 | .unwrap() | ||
| 2572 | .read(cx) | ||
| 2573 | .all_breakpoints(cx) | ||
| 2574 | .clone() | ||
| 2575 | }); | ||
| 2576 | |||
| 2577 | assert_eq!(1, breakpoints_a.len()); | ||
| 2578 | assert_eq!(breakpoints_a, breakpoints_b); | ||
| 2579 | assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len()); | ||
| 2580 | |||
| 2581 | // Client B removes first added breakpoint by client A | ||
| 2582 | editor_b.update_in(cx_b, |editor, window, cx| { | ||
| 2583 | editor.move_up(&editor::actions::MoveUp, window, cx); | ||
| 2584 | editor.move_up(&editor::actions::MoveUp, window, cx); | ||
| 2585 | editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); | ||
| 2586 | }); | ||
| 2587 | |||
| 2588 | cx_a.run_until_parked(); | ||
| 2589 | cx_b.run_until_parked(); | ||
| 2590 | |||
| 2591 | let breakpoints_a = editor_a.update(cx_a, |editor, cx| { | ||
| 2592 | editor | ||
| 2593 | .breakpoint_store() | ||
| 2594 | .clone() | ||
| 2595 | .unwrap() | ||
| 2596 | .read(cx) | ||
| 2597 | .all_breakpoints(cx) | ||
| 2598 | .clone() | ||
| 2599 | }); | ||
| 2600 | let breakpoints_b = editor_b.update(cx_b, |editor, cx| { | ||
| 2601 | editor | ||
| 2602 | .breakpoint_store() | ||
| 2603 | .clone() | ||
| 2604 | .unwrap() | ||
| 2605 | .read(cx) | ||
| 2606 | .all_breakpoints(cx) | ||
| 2607 | .clone() | ||
| 2608 | }); | ||
| 2609 | |||
| 2610 | assert_eq!(0, breakpoints_a.len()); | ||
| 2611 | assert_eq!(breakpoints_a, breakpoints_b); | ||
| 2612 | } | ||
| 2613 | |||
| 2411 | #[track_caller] | 2614 | #[track_caller] |
| 2412 | fn tab_undo_assert( | 2615 | fn tab_undo_assert( |
| 2413 | cx_a: &mut EditorTestContext, | 2616 | cx_a: &mut EditorTestContext, |
| @@ -0,0 +1,53 @@ | |||
| 1 | [package] | ||
| 2 | name = "dap" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition.workspace = true | ||
| 5 | publish.workspace = true | ||
| 6 | license = "GPL-3.0-or-later" | ||
| 7 | |||
| 8 | [lints] | ||
| 9 | workspace = true | ||
| 10 | |||
| 11 | [features] | ||
| 12 | test-support = [ | ||
| 13 | "gpui/test-support", | ||
| 14 | "util/test-support", | ||
| 15 | "task/test-support", | ||
| 16 | "async-pipe", | ||
| 17 | "settings/test-support", | ||
| 18 | ] | ||
| 19 | |||
| 20 | [dependencies] | ||
| 21 | anyhow.workspace = true | ||
| 22 | async-compression.workspace = true | ||
| 23 | async-pipe = { workspace = true, optional = true } | ||
| 24 | async-tar.workspace = true | ||
| 25 | async-trait.workspace = true | ||
| 26 | client.workspace = true | ||
| 27 | collections.workspace = true | ||
| 28 | dap-types.workspace = true | ||
| 29 | fs.workspace = true | ||
| 30 | futures.workspace = true | ||
| 31 | gpui.workspace = true | ||
| 32 | http_client.workspace = true | ||
| 33 | language.workspace = true | ||
| 34 | log.workspace = true | ||
| 35 | node_runtime.workspace = true | ||
| 36 | parking_lot.workspace = true | ||
| 37 | paths.workspace = true | ||
| 38 | schemars.workspace = true | ||
| 39 | serde.workspace = true | ||
| 40 | serde_json.workspace = true | ||
| 41 | settings.workspace = true | ||
| 42 | smallvec.workspace = true | ||
| 43 | smol.workspace = true | ||
| 44 | task.workspace = true | ||
| 45 | util.workspace = true | ||
| 46 | |||
| 47 | [dev-dependencies] | ||
| 48 | async-pipe.workspace = true | ||
| 49 | env_logger.workspace = true | ||
| 50 | gpui = { workspace = true, features = ["test-support"] } | ||
| 51 | settings = { workspace = true, features = ["test-support"] } | ||
| 52 | task = { workspace = true, features = ["test-support"] } | ||
| 53 | util = { workspace = true, features = ["test-support"] } | ||
| @@ -0,0 +1 @@ | |||
| 1 | ../../LICENSE-GPL | ||
| @@ -0,0 +1,9 @@ | |||
| 1 | # Overview | ||
| 2 | |||
| 3 | The active `Project` is responsible for maintain opened and closed breakpoints | ||
| 4 | as well as serializing breakpoints to save. At a high level project serializes | ||
| 5 | the positions of breakpoints that don't belong to any active buffers and handles | ||
| 6 | converting breakpoints from serializing to active whenever a buffer is opened/closed. | ||
| 7 | |||
| 8 | `Project` also handles sending all relevant breakpoint information to debug adapter's | ||
| 9 | during debugging or when starting a debugger. | ||
| @@ -0,0 +1,370 @@ | |||
| 1 | use ::fs::Fs; | ||
| 2 | use anyhow::{anyhow, Context as _, Ok, Result}; | ||
| 3 | use async_compression::futures::bufread::GzipDecoder; | ||
| 4 | use async_tar::Archive; | ||
| 5 | use async_trait::async_trait; | ||
| 6 | use futures::io::BufReader; | ||
| 7 | use gpui::{AsyncApp, SharedString}; | ||
| 8 | pub use http_client::{github::latest_github_release, HttpClient}; | ||
| 9 | use language::LanguageToolchainStore; | ||
| 10 | use node_runtime::NodeRuntime; | ||
| 11 | use serde::{Deserialize, Serialize}; | ||
| 12 | use serde_json::Value; | ||
| 13 | use settings::WorktreeId; | ||
| 14 | use smol::{self, fs::File, lock::Mutex}; | ||
| 15 | use std::{ | ||
| 16 | collections::{HashMap, HashSet}, | ||
| 17 | ffi::{OsStr, OsString}, | ||
| 18 | fmt::Debug, | ||
| 19 | net::Ipv4Addr, | ||
| 20 | ops::Deref, | ||
| 21 | path::{Path, PathBuf}, | ||
| 22 | sync::Arc, | ||
| 23 | }; | ||
| 24 | use task::DebugAdapterConfig; | ||
| 25 | use util::ResultExt; | ||
| 26 | |||
| 27 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
| 28 | pub enum DapStatus { | ||
| 29 | None, | ||
| 30 | CheckingForUpdate, | ||
| 31 | Downloading, | ||
| 32 | Failed { error: String }, | ||
| 33 | } | ||
| 34 | |||
| 35 | #[async_trait(?Send)] | ||
| 36 | pub trait DapDelegate { | ||
| 37 | fn worktree_id(&self) -> WorktreeId; | ||
| 38 | fn http_client(&self) -> Arc<dyn HttpClient>; | ||
| 39 | fn node_runtime(&self) -> NodeRuntime; | ||
| 40 | fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>; | ||
| 41 | fn fs(&self) -> Arc<dyn Fs>; | ||
| 42 | fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>; | ||
| 43 | fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); | ||
| 44 | fn which(&self, command: &OsStr) -> Option<PathBuf>; | ||
| 45 | async fn shell_env(&self) -> collections::HashMap<String, String>; | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] | ||
| 49 | pub struct DebugAdapterName(pub Arc<str>); | ||
| 50 | |||
| 51 | impl Deref for DebugAdapterName { | ||
| 52 | type Target = str; | ||
| 53 | |||
| 54 | fn deref(&self) -> &Self::Target { | ||
| 55 | &self.0 | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | impl AsRef<str> for DebugAdapterName { | ||
| 60 | fn as_ref(&self) -> &str { | ||
| 61 | &self.0 | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | impl AsRef<Path> for DebugAdapterName { | ||
| 66 | fn as_ref(&self) -> &Path { | ||
| 67 | Path::new(&*self.0) | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | impl std::fmt::Display for DebugAdapterName { | ||
| 72 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 73 | std::fmt::Display::fmt(&self.0, f) | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | impl From<DebugAdapterName> for SharedString { | ||
| 78 | fn from(name: DebugAdapterName) -> Self { | ||
| 79 | SharedString::from(name.0) | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | impl<'a> From<&'a str> for DebugAdapterName { | ||
| 84 | fn from(str: &'a str) -> DebugAdapterName { | ||
| 85 | DebugAdapterName(str.to_string().into()) | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | #[derive(Debug, Clone)] | ||
| 90 | pub struct TcpArguments { | ||
| 91 | pub host: Ipv4Addr, | ||
| 92 | pub port: u16, | ||
| 93 | pub timeout: Option<u64>, | ||
| 94 | } | ||
| 95 | #[derive(Debug, Clone)] | ||
| 96 | pub struct DebugAdapterBinary { | ||
| 97 | pub command: String, | ||
| 98 | pub arguments: Option<Vec<OsString>>, | ||
| 99 | pub envs: Option<HashMap<String, String>>, | ||
| 100 | pub cwd: Option<PathBuf>, | ||
| 101 | pub connection: Option<TcpArguments>, | ||
| 102 | } | ||
| 103 | |||
| 104 | pub struct AdapterVersion { | ||
| 105 | pub tag_name: String, | ||
| 106 | pub url: String, | ||
| 107 | } | ||
| 108 | |||
| 109 | pub enum DownloadedFileType { | ||
| 110 | Vsix, | ||
| 111 | GzipTar, | ||
| 112 | Zip, | ||
| 113 | } | ||
| 114 | |||
| 115 | pub struct GithubRepo { | ||
| 116 | pub repo_name: String, | ||
| 117 | pub repo_owner: String, | ||
| 118 | } | ||
| 119 | |||
| 120 | pub async fn download_adapter_from_github( | ||
| 121 | adapter_name: DebugAdapterName, | ||
| 122 | github_version: AdapterVersion, | ||
| 123 | file_type: DownloadedFileType, | ||
| 124 | delegate: &dyn DapDelegate, | ||
| 125 | ) -> Result<PathBuf> { | ||
| 126 | let adapter_path = paths::debug_adapters_dir().join(&adapter_name); | ||
| 127 | let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); | ||
| 128 | let fs = delegate.fs(); | ||
| 129 | |||
| 130 | if version_path.exists() { | ||
| 131 | return Ok(version_path); | ||
| 132 | } | ||
| 133 | |||
| 134 | if !adapter_path.exists() { | ||
| 135 | fs.create_dir(&adapter_path.as_path()) | ||
| 136 | .await | ||
| 137 | .context("Failed creating adapter path")?; | ||
| 138 | } | ||
| 139 | |||
| 140 | log::debug!( | ||
| 141 | "Downloading adapter {} from {}", | ||
| 142 | adapter_name, | ||
| 143 | &github_version.url, | ||
| 144 | ); | ||
| 145 | |||
| 146 | let mut response = delegate | ||
| 147 | .http_client() | ||
| 148 | .get(&github_version.url, Default::default(), true) | ||
| 149 | .await | ||
| 150 | .context("Error downloading release")?; | ||
| 151 | if !response.status().is_success() { | ||
| 152 | Err(anyhow!( | ||
| 153 | "download failed with status {}", | ||
| 154 | response.status().to_string() | ||
| 155 | ))?; | ||
| 156 | } | ||
| 157 | |||
| 158 | match file_type { | ||
| 159 | DownloadedFileType::GzipTar => { | ||
| 160 | let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); | ||
| 161 | let archive = Archive::new(decompressed_bytes); | ||
| 162 | archive.unpack(&version_path).await?; | ||
| 163 | } | ||
| 164 | DownloadedFileType::Zip | DownloadedFileType::Vsix => { | ||
| 165 | let zip_path = version_path.with_extension("zip"); | ||
| 166 | |||
| 167 | let mut file = File::create(&zip_path).await?; | ||
| 168 | futures::io::copy(response.body_mut(), &mut file).await?; | ||
| 169 | |||
| 170 | // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence` | ||
| 171 | util::command::new_smol_command("unzip") | ||
| 172 | .arg(&zip_path) | ||
| 173 | .arg("-d") | ||
| 174 | .arg(&version_path) | ||
| 175 | .output() | ||
| 176 | .await?; | ||
| 177 | |||
| 178 | util::fs::remove_matching(&adapter_path, |entry| { | ||
| 179 | entry | ||
| 180 | .file_name() | ||
| 181 | .is_some_and(|file| file.to_string_lossy().ends_with(".zip")) | ||
| 182 | }) | ||
| 183 | .await; | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | // remove older versions | ||
| 188 | util::fs::remove_matching(&adapter_path, |entry| { | ||
| 189 | entry.to_string_lossy() != version_path.to_string_lossy() | ||
| 190 | }) | ||
| 191 | .await; | ||
| 192 | |||
| 193 | Ok(version_path) | ||
| 194 | } | ||
| 195 | |||
| 196 | pub async fn fetch_latest_adapter_version_from_github( | ||
| 197 | github_repo: GithubRepo, | ||
| 198 | delegate: &dyn DapDelegate, | ||
| 199 | ) -> Result<AdapterVersion> { | ||
| 200 | let release = latest_github_release( | ||
| 201 | &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), | ||
| 202 | false, | ||
| 203 | false, | ||
| 204 | delegate.http_client(), | ||
| 205 | ) | ||
| 206 | .await?; | ||
| 207 | |||
| 208 | Ok(AdapterVersion { | ||
| 209 | tag_name: release.tag_name, | ||
| 210 | url: release.zipball_url, | ||
| 211 | }) | ||
| 212 | } | ||
| 213 | |||
| 214 | #[async_trait(?Send)] | ||
| 215 | pub trait DebugAdapter: 'static + Send + Sync { | ||
| 216 | fn name(&self) -> DebugAdapterName; | ||
| 217 | |||
| 218 | async fn get_binary( | ||
| 219 | &self, | ||
| 220 | delegate: &dyn DapDelegate, | ||
| 221 | config: &DebugAdapterConfig, | ||
| 222 | user_installed_path: Option<PathBuf>, | ||
| 223 | cx: &mut AsyncApp, | ||
| 224 | ) -> Result<DebugAdapterBinary> { | ||
| 225 | if delegate | ||
| 226 | .updated_adapters() | ||
| 227 | .lock() | ||
| 228 | .await | ||
| 229 | .contains(&self.name()) | ||
| 230 | { | ||
| 231 | log::info!("Using cached debug adapter binary {}", self.name()); | ||
| 232 | |||
| 233 | if let Some(binary) = self | ||
| 234 | .get_installed_binary(delegate, &config, user_installed_path.clone(), cx) | ||
| 235 | .await | ||
| 236 | .log_err() | ||
| 237 | { | ||
| 238 | return Ok(binary); | ||
| 239 | } | ||
| 240 | |||
| 241 | log::info!( | ||
| 242 | "Cached binary {} is corrupt falling back to install", | ||
| 243 | self.name() | ||
| 244 | ); | ||
| 245 | } | ||
| 246 | |||
| 247 | log::info!("Getting latest version of debug adapter {}", self.name()); | ||
| 248 | delegate.update_status(self.name(), DapStatus::CheckingForUpdate); | ||
| 249 | if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { | ||
| 250 | log::info!( | ||
| 251 | "Installiing latest version of debug adapter {}", | ||
| 252 | self.name() | ||
| 253 | ); | ||
| 254 | delegate.update_status(self.name(), DapStatus::Downloading); | ||
| 255 | self.install_binary(version, delegate).await?; | ||
| 256 | |||
| 257 | delegate | ||
| 258 | .updated_adapters() | ||
| 259 | .lock_arc() | ||
| 260 | .await | ||
| 261 | .insert(self.name()); | ||
| 262 | } | ||
| 263 | |||
| 264 | self.get_installed_binary(delegate, &config, user_installed_path, cx) | ||
| 265 | .await | ||
| 266 | } | ||
| 267 | |||
| 268 | async fn fetch_latest_adapter_version( | ||
| 269 | &self, | ||
| 270 | delegate: &dyn DapDelegate, | ||
| 271 | ) -> Result<AdapterVersion>; | ||
| 272 | |||
| 273 | /// Installs the binary for the debug adapter. | ||
| 274 | /// This method is called when the adapter binary is not found or needs to be updated. | ||
| 275 | /// It should download and install the necessary files for the debug adapter to function. | ||
| 276 | async fn install_binary( | ||
| 277 | &self, | ||
| 278 | version: AdapterVersion, | ||
| 279 | delegate: &dyn DapDelegate, | ||
| 280 | ) -> Result<()>; | ||
| 281 | |||
| 282 | async fn get_installed_binary( | ||
| 283 | &self, | ||
| 284 | delegate: &dyn DapDelegate, | ||
| 285 | config: &DebugAdapterConfig, | ||
| 286 | user_installed_path: Option<PathBuf>, | ||
| 287 | cx: &mut AsyncApp, | ||
| 288 | ) -> Result<DebugAdapterBinary>; | ||
| 289 | |||
| 290 | /// Should return base configuration to make the debug adapter work | ||
| 291 | fn request_args(&self, config: &DebugAdapterConfig) -> Value; | ||
| 292 | } | ||
| 293 | |||
| 294 | #[cfg(any(test, feature = "test-support"))] | ||
| 295 | pub struct FakeAdapter {} | ||
| 296 | |||
| 297 | #[cfg(any(test, feature = "test-support"))] | ||
| 298 | impl FakeAdapter { | ||
| 299 | const ADAPTER_NAME: &'static str = "fake-adapter"; | ||
| 300 | |||
| 301 | pub fn new() -> Self { | ||
| 302 | Self {} | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | #[cfg(any(test, feature = "test-support"))] | ||
| 307 | #[async_trait(?Send)] | ||
| 308 | impl DebugAdapter for FakeAdapter { | ||
| 309 | fn name(&self) -> DebugAdapterName { | ||
| 310 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 311 | } | ||
| 312 | |||
| 313 | async fn get_binary( | ||
| 314 | &self, | ||
| 315 | _: &dyn DapDelegate, | ||
| 316 | _: &DebugAdapterConfig, | ||
| 317 | _: Option<PathBuf>, | ||
| 318 | _: &mut AsyncApp, | ||
| 319 | ) -> Result<DebugAdapterBinary> { | ||
| 320 | Ok(DebugAdapterBinary { | ||
| 321 | command: "command".into(), | ||
| 322 | arguments: None, | ||
| 323 | connection: None, | ||
| 324 | envs: None, | ||
| 325 | cwd: None, | ||
| 326 | }) | ||
| 327 | } | ||
| 328 | |||
| 329 | async fn fetch_latest_adapter_version( | ||
| 330 | &self, | ||
| 331 | _delegate: &dyn DapDelegate, | ||
| 332 | ) -> Result<AdapterVersion> { | ||
| 333 | unimplemented!("fetch latest adapter version"); | ||
| 334 | } | ||
| 335 | |||
| 336 | async fn install_binary( | ||
| 337 | &self, | ||
| 338 | _version: AdapterVersion, | ||
| 339 | _delegate: &dyn DapDelegate, | ||
| 340 | ) -> Result<()> { | ||
| 341 | unimplemented!("install binary"); | ||
| 342 | } | ||
| 343 | |||
| 344 | async fn get_installed_binary( | ||
| 345 | &self, | ||
| 346 | _: &dyn DapDelegate, | ||
| 347 | _: &DebugAdapterConfig, | ||
| 348 | _: Option<PathBuf>, | ||
| 349 | _: &mut AsyncApp, | ||
| 350 | ) -> Result<DebugAdapterBinary> { | ||
| 351 | unimplemented!("get installed binary"); | ||
| 352 | } | ||
| 353 | |||
| 354 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 355 | use serde_json::json; | ||
| 356 | use task::DebugRequestType; | ||
| 357 | |||
| 358 | json!({ | ||
| 359 | "request": match config.request { | ||
| 360 | DebugRequestType::Launch => "launch", | ||
| 361 | DebugRequestType::Attach(_) => "attach", | ||
| 362 | }, | ||
| 363 | "process_id": if let DebugRequestType::Attach(attach_config) = &config.request { | ||
| 364 | attach_config.process_id | ||
| 365 | } else { | ||
| 366 | None | ||
| 367 | }, | ||
| 368 | }) | ||
| 369 | } | ||
| 370 | } | ||
| @@ -0,0 +1,490 @@ | |||
| 1 | use crate::{ | ||
| 2 | adapters::{DebugAdapterBinary, DebugAdapterName}, | ||
| 3 | transport::{IoKind, LogKind, TransportDelegate}, | ||
| 4 | }; | ||
| 5 | use anyhow::{anyhow, Result}; | ||
| 6 | use dap_types::{ | ||
| 7 | messages::{Message, Response}, | ||
| 8 | requests::Request, | ||
| 9 | }; | ||
| 10 | use futures::{channel::oneshot, select, FutureExt as _}; | ||
| 11 | use gpui::{AppContext, AsyncApp, BackgroundExecutor}; | ||
| 12 | use smol::channel::{Receiver, Sender}; | ||
| 13 | use std::{ | ||
| 14 | hash::Hash, | ||
| 15 | sync::atomic::{AtomicU64, Ordering}, | ||
| 16 | time::Duration, | ||
| 17 | }; | ||
| 18 | |||
| 19 | #[cfg(any(test, feature = "test-support"))] | ||
| 20 | const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2); | ||
| 21 | |||
| 22 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 23 | const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); | ||
| 24 | |||
| 25 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
| 26 | #[repr(transparent)] | ||
| 27 | pub struct SessionId(pub u32); | ||
| 28 | |||
| 29 | impl SessionId { | ||
| 30 | pub fn from_proto(client_id: u64) -> Self { | ||
| 31 | Self(client_id as u32) | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn to_proto(&self) -> u64 { | ||
| 35 | self.0 as u64 | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | /// Represents a connection to the debug adapter process, either via stdout/stdin or a socket. | ||
| 40 | pub struct DebugAdapterClient { | ||
| 41 | id: SessionId, | ||
| 42 | name: DebugAdapterName, | ||
| 43 | sequence_count: AtomicU64, | ||
| 44 | binary: DebugAdapterBinary, | ||
| 45 | executor: BackgroundExecutor, | ||
| 46 | transport_delegate: TransportDelegate, | ||
| 47 | } | ||
| 48 | |||
| 49 | pub type DapMessageHandler = Box<dyn FnMut(Message) + 'static + Send + Sync>; | ||
| 50 | |||
| 51 | impl DebugAdapterClient { | ||
| 52 | pub async fn start( | ||
| 53 | id: SessionId, | ||
| 54 | name: DebugAdapterName, | ||
| 55 | binary: DebugAdapterBinary, | ||
| 56 | message_handler: DapMessageHandler, | ||
| 57 | cx: AsyncApp, | ||
| 58 | ) -> Result<Self> { | ||
| 59 | let ((server_rx, server_tx), transport_delegate) = | ||
| 60 | TransportDelegate::start(&binary, cx.clone()).await?; | ||
| 61 | let this = Self { | ||
| 62 | id, | ||
| 63 | name, | ||
| 64 | binary, | ||
| 65 | transport_delegate, | ||
| 66 | sequence_count: AtomicU64::new(1), | ||
| 67 | executor: cx.background_executor().clone(), | ||
| 68 | }; | ||
| 69 | log::info!("Successfully connected to debug adapter"); | ||
| 70 | |||
| 71 | let client_id = this.id; | ||
| 72 | |||
| 73 | // start handling events/reverse requests | ||
| 74 | |||
| 75 | cx.background_spawn(Self::handle_receive_messages( | ||
| 76 | client_id, | ||
| 77 | server_rx, | ||
| 78 | server_tx.clone(), | ||
| 79 | message_handler, | ||
| 80 | )) | ||
| 81 | .detach(); | ||
| 82 | |||
| 83 | Ok(this) | ||
| 84 | } | ||
| 85 | |||
| 86 | pub async fn reconnect( | ||
| 87 | &self, | ||
| 88 | session_id: SessionId, | ||
| 89 | binary: DebugAdapterBinary, | ||
| 90 | message_handler: DapMessageHandler, | ||
| 91 | cx: AsyncApp, | ||
| 92 | ) -> Result<Self> { | ||
| 93 | let binary = match self.transport_delegate.transport() { | ||
| 94 | crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary { | ||
| 95 | command: binary.command, | ||
| 96 | arguments: binary.arguments, | ||
| 97 | envs: binary.envs, | ||
| 98 | cwd: binary.cwd, | ||
| 99 | connection: Some(crate::adapters::TcpArguments { | ||
| 100 | host: tcp_transport.host, | ||
| 101 | port: tcp_transport.port, | ||
| 102 | timeout: Some(tcp_transport.timeout), | ||
| 103 | }), | ||
| 104 | }, | ||
| 105 | _ => self.binary.clone(), | ||
| 106 | }; | ||
| 107 | |||
| 108 | Self::start(session_id, self.name(), binary, message_handler, cx).await | ||
| 109 | } | ||
| 110 | |||
| 111 | async fn handle_receive_messages( | ||
| 112 | client_id: SessionId, | ||
| 113 | server_rx: Receiver<Message>, | ||
| 114 | client_tx: Sender<Message>, | ||
| 115 | mut message_handler: DapMessageHandler, | ||
| 116 | ) -> Result<()> { | ||
| 117 | let result = loop { | ||
| 118 | let message = match server_rx.recv().await { | ||
| 119 | Ok(message) => message, | ||
| 120 | Err(e) => break Err(e.into()), | ||
| 121 | }; | ||
| 122 | |||
| 123 | match message { | ||
| 124 | Message::Event(ev) => { | ||
| 125 | log::debug!("Client {} received event `{}`", client_id.0, &ev); | ||
| 126 | |||
| 127 | message_handler(Message::Event(ev)) | ||
| 128 | } | ||
| 129 | Message::Request(req) => { | ||
| 130 | log::debug!( | ||
| 131 | "Client {} received reverse request `{}`", | ||
| 132 | client_id.0, | ||
| 133 | &req.command | ||
| 134 | ); | ||
| 135 | |||
| 136 | message_handler(Message::Request(req)) | ||
| 137 | } | ||
| 138 | Message::Response(response) => { | ||
| 139 | log::debug!("Received response after request timeout: {:#?}", response); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | smol::future::yield_now().await; | ||
| 144 | }; | ||
| 145 | |||
| 146 | drop(client_tx); | ||
| 147 | |||
| 148 | log::debug!("Handle receive messages dropped"); | ||
| 149 | |||
| 150 | result | ||
| 151 | } | ||
| 152 | |||
| 153 | /// Send a request to an adapter and get a response back | ||
| 154 | /// Note: This function will block until a response is sent back from the adapter | ||
| 155 | pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> { | ||
| 156 | let serialized_arguments = serde_json::to_value(arguments)?; | ||
| 157 | |||
| 158 | let (callback_tx, callback_rx) = oneshot::channel::<Result<Response>>(); | ||
| 159 | |||
| 160 | let sequence_id = self.next_sequence_id(); | ||
| 161 | |||
| 162 | let request = crate::messages::Request { | ||
| 163 | seq: sequence_id, | ||
| 164 | command: R::COMMAND.to_string(), | ||
| 165 | arguments: Some(serialized_arguments), | ||
| 166 | }; | ||
| 167 | |||
| 168 | self.transport_delegate | ||
| 169 | .add_pending_request(sequence_id, callback_tx) | ||
| 170 | .await; | ||
| 171 | |||
| 172 | log::debug!( | ||
| 173 | "Client {} send `{}` request with sequence_id: {}", | ||
| 174 | self.id.0, | ||
| 175 | R::COMMAND.to_string(), | ||
| 176 | sequence_id | ||
| 177 | ); | ||
| 178 | |||
| 179 | self.send_message(Message::Request(request)).await?; | ||
| 180 | |||
| 181 | let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse(); | ||
| 182 | let command = R::COMMAND.to_string(); | ||
| 183 | |||
| 184 | select! { | ||
| 185 | response = callback_rx.fuse() => { | ||
| 186 | log::debug!( | ||
| 187 | "Client {} received response for: `{}` sequence_id: {}", | ||
| 188 | self.id.0, | ||
| 189 | command, | ||
| 190 | sequence_id | ||
| 191 | ); | ||
| 192 | |||
| 193 | let response = response??; | ||
| 194 | match response.success { | ||
| 195 | true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), | ||
| 196 | false => Err(anyhow!("Request failed: {}", response.message.unwrap_or_default())), | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | _ = timeout => { | ||
| 201 | self.transport_delegate.cancel_pending_request(&sequence_id).await; | ||
| 202 | log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}"); | ||
| 203 | anyhow::bail!("DAP request timeout"); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | pub async fn send_message(&self, message: Message) -> Result<()> { | ||
| 209 | self.transport_delegate.send_message(message).await | ||
| 210 | } | ||
| 211 | |||
| 212 | pub fn id(&self) -> SessionId { | ||
| 213 | self.id | ||
| 214 | } | ||
| 215 | |||
| 216 | pub fn name(&self) -> DebugAdapterName { | ||
| 217 | self.name.clone() | ||
| 218 | } | ||
| 219 | pub fn binary(&self) -> &DebugAdapterBinary { | ||
| 220 | &self.binary | ||
| 221 | } | ||
| 222 | |||
| 223 | /// Get the next sequence id to be used in a request | ||
| 224 | pub fn next_sequence_id(&self) -> u64 { | ||
| 225 | self.sequence_count.fetch_add(1, Ordering::Relaxed) | ||
| 226 | } | ||
| 227 | |||
| 228 | pub async fn shutdown(&self) -> Result<()> { | ||
| 229 | self.transport_delegate.shutdown().await | ||
| 230 | } | ||
| 231 | |||
| 232 | pub fn has_adapter_logs(&self) -> bool { | ||
| 233 | self.transport_delegate.has_adapter_logs() | ||
| 234 | } | ||
| 235 | |||
| 236 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) | ||
| 237 | where | ||
| 238 | F: 'static + Send + FnMut(IoKind, &str), | ||
| 239 | { | ||
| 240 | self.transport_delegate.add_log_handler(f, kind); | ||
| 241 | } | ||
| 242 | |||
| 243 | #[cfg(any(test, feature = "test-support"))] | ||
| 244 | pub async fn on_request<R: dap_types::requests::Request, F>(&self, handler: F) | ||
| 245 | where | ||
| 246 | F: 'static | ||
| 247 | + Send | ||
| 248 | + FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>, | ||
| 249 | { | ||
| 250 | let transport = self.transport_delegate.transport().as_fake(); | ||
| 251 | transport.on_request::<R, F>(handler).await; | ||
| 252 | } | ||
| 253 | |||
| 254 | #[cfg(any(test, feature = "test-support"))] | ||
| 255 | pub async fn fake_reverse_request<R: dap_types::requests::Request>(&self, args: R::Arguments) { | ||
| 256 | self.send_message(Message::Request(dap_types::messages::Request { | ||
| 257 | seq: self.sequence_count.load(Ordering::Relaxed), | ||
| 258 | command: R::COMMAND.into(), | ||
| 259 | arguments: serde_json::to_value(args).ok(), | ||
| 260 | })) | ||
| 261 | .await | ||
| 262 | .unwrap(); | ||
| 263 | } | ||
| 264 | |||
| 265 | #[cfg(any(test, feature = "test-support"))] | ||
| 266 | pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F) | ||
| 267 | where | ||
| 268 | F: 'static + Send + Fn(Response), | ||
| 269 | { | ||
| 270 | let transport = self.transport_delegate.transport().as_fake(); | ||
| 271 | transport.on_response::<R, F>(handler).await; | ||
| 272 | } | ||
| 273 | |||
| 274 | #[cfg(any(test, feature = "test-support"))] | ||
| 275 | pub async fn fake_event(&self, event: dap_types::messages::Events) { | ||
| 276 | self.send_message(Message::Event(Box::new(event))) | ||
| 277 | .await | ||
| 278 | .unwrap(); | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | #[cfg(test)] | ||
| 283 | mod tests { | ||
| 284 | use super::*; | ||
| 285 | use crate::{client::DebugAdapterClient, debugger_settings::DebuggerSettings}; | ||
| 286 | use dap_types::{ | ||
| 287 | messages::Events, | ||
| 288 | requests::{Initialize, Request, RunInTerminal}, | ||
| 289 | Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, | ||
| 290 | RunInTerminalRequestArguments, | ||
| 291 | }; | ||
| 292 | use gpui::TestAppContext; | ||
| 293 | use serde_json::json; | ||
| 294 | use settings::{Settings, SettingsStore}; | ||
| 295 | use std::sync::{ | ||
| 296 | atomic::{AtomicBool, Ordering}, | ||
| 297 | Arc, | ||
| 298 | }; | ||
| 299 | |||
| 300 | pub fn init_test(cx: &mut gpui::TestAppContext) { | ||
| 301 | if std::env::var("RUST_LOG").is_ok() { | ||
| 302 | env_logger::try_init().ok(); | ||
| 303 | } | ||
| 304 | |||
| 305 | cx.update(|cx| { | ||
| 306 | let settings = SettingsStore::test(cx); | ||
| 307 | cx.set_global(settings); | ||
| 308 | DebuggerSettings::register(cx); | ||
| 309 | }); | ||
| 310 | } | ||
| 311 | |||
| 312 | #[gpui::test] | ||
| 313 | pub async fn test_initialize_client(cx: &mut TestAppContext) { | ||
| 314 | init_test(cx); | ||
| 315 | |||
| 316 | let client = DebugAdapterClient::start( | ||
| 317 | crate::client::SessionId(1), | ||
| 318 | DebugAdapterName("adapter".into()), | ||
| 319 | DebugAdapterBinary { | ||
| 320 | command: "command".into(), | ||
| 321 | arguments: Default::default(), | ||
| 322 | envs: Default::default(), | ||
| 323 | connection: None, | ||
| 324 | cwd: None, | ||
| 325 | }, | ||
| 326 | Box::new(|_| panic!("Did not expect to hit this code path")), | ||
| 327 | cx.to_async(), | ||
| 328 | ) | ||
| 329 | .await | ||
| 330 | .unwrap(); | ||
| 331 | |||
| 332 | client | ||
| 333 | .on_request::<Initialize, _>(move |_, _| { | ||
| 334 | Ok(dap_types::Capabilities { | ||
| 335 | supports_configuration_done_request: Some(true), | ||
| 336 | ..Default::default() | ||
| 337 | }) | ||
| 338 | }) | ||
| 339 | .await; | ||
| 340 | |||
| 341 | cx.run_until_parked(); | ||
| 342 | |||
| 343 | let response = client | ||
| 344 | .request::<Initialize>(InitializeRequestArguments { | ||
| 345 | client_id: Some("zed".to_owned()), | ||
| 346 | client_name: Some("Zed".to_owned()), | ||
| 347 | adapter_id: "fake-adapter".to_owned(), | ||
| 348 | locale: Some("en-US".to_owned()), | ||
| 349 | path_format: Some(InitializeRequestArgumentsPathFormat::Path), | ||
| 350 | supports_variable_type: Some(true), | ||
| 351 | supports_variable_paging: Some(false), | ||
| 352 | supports_run_in_terminal_request: Some(true), | ||
| 353 | supports_memory_references: Some(true), | ||
| 354 | supports_progress_reporting: Some(false), | ||
| 355 | supports_invalidated_event: Some(false), | ||
| 356 | lines_start_at1: Some(true), | ||
| 357 | columns_start_at1: Some(true), | ||
| 358 | supports_memory_event: Some(false), | ||
| 359 | supports_args_can_be_interpreted_by_shell: Some(false), | ||
| 360 | supports_start_debugging_request: Some(true), | ||
| 361 | supports_ansistyling: Some(false), | ||
| 362 | }) | ||
| 363 | .await | ||
| 364 | .unwrap(); | ||
| 365 | |||
| 366 | cx.run_until_parked(); | ||
| 367 | |||
| 368 | assert_eq!( | ||
| 369 | dap_types::Capabilities { | ||
| 370 | supports_configuration_done_request: Some(true), | ||
| 371 | ..Default::default() | ||
| 372 | }, | ||
| 373 | response | ||
| 374 | ); | ||
| 375 | |||
| 376 | client.shutdown().await.unwrap(); | ||
| 377 | } | ||
| 378 | |||
| 379 | #[gpui::test] | ||
| 380 | pub async fn test_calls_event_handler(cx: &mut TestAppContext) { | ||
| 381 | init_test(cx); | ||
| 382 | |||
| 383 | let called_event_handler = Arc::new(AtomicBool::new(false)); | ||
| 384 | |||
| 385 | let client = DebugAdapterClient::start( | ||
| 386 | crate::client::SessionId(1), | ||
| 387 | DebugAdapterName("adapter".into()), | ||
| 388 | DebugAdapterBinary { | ||
| 389 | command: "command".into(), | ||
| 390 | arguments: Default::default(), | ||
| 391 | envs: Default::default(), | ||
| 392 | connection: None, | ||
| 393 | cwd: None, | ||
| 394 | }, | ||
| 395 | Box::new({ | ||
| 396 | let called_event_handler = called_event_handler.clone(); | ||
| 397 | move |event| { | ||
| 398 | called_event_handler.store(true, Ordering::SeqCst); | ||
| 399 | |||
| 400 | assert_eq!( | ||
| 401 | Message::Event(Box::new(Events::Initialized( | ||
| 402 | Some(Capabilities::default()) | ||
| 403 | ))), | ||
| 404 | event | ||
| 405 | ); | ||
| 406 | } | ||
| 407 | }), | ||
| 408 | cx.to_async(), | ||
| 409 | ) | ||
| 410 | .await | ||
| 411 | .unwrap(); | ||
| 412 | |||
| 413 | cx.run_until_parked(); | ||
| 414 | |||
| 415 | client | ||
| 416 | .fake_event(Events::Initialized(Some(Capabilities::default()))) | ||
| 417 | .await; | ||
| 418 | |||
| 419 | cx.run_until_parked(); | ||
| 420 | |||
| 421 | assert!( | ||
| 422 | called_event_handler.load(std::sync::atomic::Ordering::SeqCst), | ||
| 423 | "Event handler was not called" | ||
| 424 | ); | ||
| 425 | |||
| 426 | client.shutdown().await.unwrap(); | ||
| 427 | } | ||
| 428 | |||
| 429 | #[gpui::test] | ||
| 430 | pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) { | ||
| 431 | init_test(cx); | ||
| 432 | |||
| 433 | let called_event_handler = Arc::new(AtomicBool::new(false)); | ||
| 434 | |||
| 435 | let client = DebugAdapterClient::start( | ||
| 436 | crate::client::SessionId(1), | ||
| 437 | DebugAdapterName(Arc::from("test-adapter")), | ||
| 438 | DebugAdapterBinary { | ||
| 439 | command: "command".into(), | ||
| 440 | arguments: Default::default(), | ||
| 441 | envs: Default::default(), | ||
| 442 | connection: None, | ||
| 443 | cwd: None, | ||
| 444 | }, | ||
| 445 | Box::new({ | ||
| 446 | let called_event_handler = called_event_handler.clone(); | ||
| 447 | move |event| { | ||
| 448 | called_event_handler.store(true, Ordering::SeqCst); | ||
| 449 | |||
| 450 | assert_eq!( | ||
| 451 | Message::Request(dap_types::messages::Request { | ||
| 452 | seq: 1, | ||
| 453 | command: RunInTerminal::COMMAND.into(), | ||
| 454 | arguments: Some(json!({ | ||
| 455 | "cwd": "/project/path/src", | ||
| 456 | "args": ["node", "test.js"], | ||
| 457 | })) | ||
| 458 | }), | ||
| 459 | event | ||
| 460 | ); | ||
| 461 | } | ||
| 462 | }), | ||
| 463 | cx.to_async(), | ||
| 464 | ) | ||
| 465 | .await | ||
| 466 | .unwrap(); | ||
| 467 | |||
| 468 | cx.run_until_parked(); | ||
| 469 | |||
| 470 | client | ||
| 471 | .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments { | ||
| 472 | kind: None, | ||
| 473 | title: None, | ||
| 474 | cwd: "/project/path/src".into(), | ||
| 475 | args: vec!["node".into(), "test.js".into()], | ||
| 476 | env: None, | ||
| 477 | args_can_be_interpreted_by_shell: None, | ||
| 478 | }) | ||
| 479 | .await; | ||
| 480 | |||
| 481 | cx.run_until_parked(); | ||
| 482 | |||
| 483 | assert!( | ||
| 484 | called_event_handler.load(std::sync::atomic::Ordering::SeqCst), | ||
| 485 | "Event handler was not called" | ||
| 486 | ); | ||
| 487 | |||
| 488 | client.shutdown().await.unwrap(); | ||
| 489 | } | ||
| 490 | } | ||
| @@ -0,0 +1,59 @@ | |||
| 1 | use dap_types::SteppingGranularity; | ||
| 2 | use gpui::{App, Global}; | ||
| 3 | use schemars::JsonSchema; | ||
| 4 | use serde::{Deserialize, Serialize}; | ||
| 5 | use settings::{Settings, SettingsSources}; | ||
| 6 | |||
| 7 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] | ||
| 8 | #[serde(default)] | ||
| 9 | pub struct DebuggerSettings { | ||
| 10 | /// Determines the stepping granularity. | ||
| 11 | /// | ||
| 12 | /// Default: line | ||
| 13 | pub stepping_granularity: SteppingGranularity, | ||
| 14 | /// Whether the breakpoints should be reused across Zed sessions. | ||
| 15 | /// | ||
| 16 | /// Default: true | ||
| 17 | pub save_breakpoints: bool, | ||
| 18 | /// Whether to show the debug button in the status bar. | ||
| 19 | /// | ||
| 20 | /// Default: true | ||
| 21 | pub button: bool, | ||
| 22 | /// Time in milliseconds until timeout error when connecting to a TCP debug adapter | ||
| 23 | /// | ||
| 24 | /// Default: 2000ms | ||
| 25 | pub timeout: u64, | ||
| 26 | /// Whether to log messages between active debug adapters and Zed | ||
| 27 | /// | ||
| 28 | /// Default: true | ||
| 29 | pub log_dap_communications: bool, | ||
| 30 | /// Whether to format dap messages in when adding them to debug adapter logger | ||
| 31 | /// | ||
| 32 | /// Default: true | ||
| 33 | pub format_dap_log_messages: bool, | ||
| 34 | } | ||
| 35 | |||
| 36 | impl Default for DebuggerSettings { | ||
| 37 | fn default() -> Self { | ||
| 38 | Self { | ||
| 39 | button: true, | ||
| 40 | save_breakpoints: true, | ||
| 41 | stepping_granularity: SteppingGranularity::Line, | ||
| 42 | timeout: 2000, | ||
| 43 | log_dap_communications: true, | ||
| 44 | format_dap_log_messages: true, | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | impl Settings for DebuggerSettings { | ||
| 50 | const KEY: Option<&'static str> = Some("debugger"); | ||
| 51 | |||
| 52 | type FileContent = Self; | ||
| 53 | |||
| 54 | fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> { | ||
| 55 | sources.json_merge() | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | impl Global for DebuggerSettings {} | ||
| @@ -0,0 +1,38 @@ | |||
| 1 | pub mod adapters; | ||
| 2 | pub mod client; | ||
| 3 | pub mod debugger_settings; | ||
| 4 | pub mod proto_conversions; | ||
| 5 | pub mod transport; | ||
| 6 | |||
| 7 | pub use dap_types::*; | ||
| 8 | pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType}; | ||
| 9 | |||
| 10 | pub type ScopeId = u64; | ||
| 11 | pub type VariableReference = u64; | ||
| 12 | pub type StackFrameId = u64; | ||
| 13 | |||
| 14 | #[cfg(any(test, feature = "test-support"))] | ||
| 15 | pub use adapters::FakeAdapter; | ||
| 16 | |||
| 17 | #[cfg(any(test, feature = "test-support"))] | ||
| 18 | pub fn test_config( | ||
| 19 | request: DebugRequestType, | ||
| 20 | fail: Option<bool>, | ||
| 21 | caps: Option<Capabilities>, | ||
| 22 | ) -> DebugAdapterConfig { | ||
| 23 | DebugAdapterConfig { | ||
| 24 | label: "test config".into(), | ||
| 25 | kind: DebugAdapterKind::Fake(( | ||
| 26 | fail.unwrap_or_default(), | ||
| 27 | caps.unwrap_or(Capabilities { | ||
| 28 | supports_step_back: Some(false), | ||
| 29 | ..Default::default() | ||
| 30 | }), | ||
| 31 | )), | ||
| 32 | request, | ||
| 33 | program: None, | ||
| 34 | supports_attach: false, | ||
| 35 | cwd: None, | ||
| 36 | initialize_args: None, | ||
| 37 | } | ||
| 38 | } | ||
| @@ -0,0 +1,591 @@ | |||
| 1 | use anyhow::{anyhow, Result}; | ||
| 2 | use client::proto::{ | ||
| 3 | self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope, | ||
| 4 | DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, | ||
| 5 | }; | ||
| 6 | use dap_types::{OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source}; | ||
| 7 | |||
| 8 | pub trait ProtoConversion { | ||
| 9 | type ProtoType; | ||
| 10 | type Output; | ||
| 11 | |||
| 12 | fn to_proto(&self) -> Self::ProtoType; | ||
| 13 | fn from_proto(payload: Self::ProtoType) -> Self::Output; | ||
| 14 | } | ||
| 15 | |||
| 16 | impl<T> ProtoConversion for Vec<T> | ||
| 17 | where | ||
| 18 | T: ProtoConversion<Output = T>, | ||
| 19 | { | ||
| 20 | type ProtoType = Vec<T::ProtoType>; | ||
| 21 | type Output = Self; | ||
| 22 | |||
| 23 | fn to_proto(&self) -> Self::ProtoType { | ||
| 24 | self.iter().map(|item| item.to_proto()).collect() | ||
| 25 | } | ||
| 26 | |||
| 27 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 28 | payload | ||
| 29 | .into_iter() | ||
| 30 | .map(|item| T::from_proto(item)) | ||
| 31 | .collect() | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | impl ProtoConversion for dap_types::Scope { | ||
| 36 | type ProtoType = DapScope; | ||
| 37 | type Output = Self; | ||
| 38 | |||
| 39 | fn to_proto(&self) -> Self::ProtoType { | ||
| 40 | Self::ProtoType { | ||
| 41 | name: self.name.clone(), | ||
| 42 | presentation_hint: self | ||
| 43 | .presentation_hint | ||
| 44 | .as_ref() | ||
| 45 | .map(|hint| hint.to_proto().into()), | ||
| 46 | variables_reference: self.variables_reference, | ||
| 47 | named_variables: self.named_variables, | ||
| 48 | indexed_variables: self.indexed_variables, | ||
| 49 | expensive: self.expensive, | ||
| 50 | source: self.source.as_ref().map(Source::to_proto), | ||
| 51 | line: self.line, | ||
| 52 | end_line: self.end_line, | ||
| 53 | column: self.column, | ||
| 54 | end_column: self.end_column, | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 59 | let presentation_hint = payload | ||
| 60 | .presentation_hint | ||
| 61 | .and_then(DapScopePresentationHint::from_i32); | ||
| 62 | Self { | ||
| 63 | name: payload.name, | ||
| 64 | presentation_hint: presentation_hint.map(ScopePresentationHint::from_proto), | ||
| 65 | variables_reference: payload.variables_reference, | ||
| 66 | named_variables: payload.named_variables, | ||
| 67 | indexed_variables: payload.indexed_variables, | ||
| 68 | expensive: payload.expensive, | ||
| 69 | source: payload.source.map(dap_types::Source::from_proto), | ||
| 70 | line: payload.line, | ||
| 71 | end_line: payload.end_line, | ||
| 72 | column: payload.column, | ||
| 73 | end_column: payload.end_column, | ||
| 74 | } | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | impl ProtoConversion for dap_types::Variable { | ||
| 79 | type ProtoType = DapVariable; | ||
| 80 | type Output = Self; | ||
| 81 | |||
| 82 | fn to_proto(&self) -> Self::ProtoType { | ||
| 83 | Self::ProtoType { | ||
| 84 | name: self.name.clone(), | ||
| 85 | value: self.value.clone(), | ||
| 86 | r#type: self.type_.clone(), | ||
| 87 | evaluate_name: self.evaluate_name.clone(), | ||
| 88 | variables_reference: self.variables_reference, | ||
| 89 | named_variables: self.named_variables, | ||
| 90 | indexed_variables: self.indexed_variables, | ||
| 91 | memory_reference: self.memory_reference.clone(), | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 96 | Self { | ||
| 97 | name: payload.name, | ||
| 98 | value: payload.value, | ||
| 99 | type_: payload.r#type, | ||
| 100 | evaluate_name: payload.evaluate_name, | ||
| 101 | presentation_hint: None, // TODO Debugger Collab Add this | ||
| 102 | variables_reference: payload.variables_reference, | ||
| 103 | named_variables: payload.named_variables, | ||
| 104 | indexed_variables: payload.indexed_variables, | ||
| 105 | memory_reference: payload.memory_reference, | ||
| 106 | declaration_location_reference: None, // TODO | ||
| 107 | value_location_reference: None, // TODO | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | impl ProtoConversion for dap_types::ScopePresentationHint { | ||
| 113 | type ProtoType = DapScopePresentationHint; | ||
| 114 | type Output = Self; | ||
| 115 | |||
| 116 | fn to_proto(&self) -> Self::ProtoType { | ||
| 117 | match self { | ||
| 118 | dap_types::ScopePresentationHint::Locals => DapScopePresentationHint::Locals, | ||
| 119 | dap_types::ScopePresentationHint::Arguments => DapScopePresentationHint::Arguments, | ||
| 120 | dap_types::ScopePresentationHint::Registers => DapScopePresentationHint::Registers, | ||
| 121 | dap_types::ScopePresentationHint::ReturnValue => DapScopePresentationHint::ReturnValue, | ||
| 122 | dap_types::ScopePresentationHint::Unknown => DapScopePresentationHint::ScopeUnknown, | ||
| 123 | &_ => unreachable!(), | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 128 | match payload { | ||
| 129 | DapScopePresentationHint::Locals => dap_types::ScopePresentationHint::Locals, | ||
| 130 | DapScopePresentationHint::Arguments => dap_types::ScopePresentationHint::Arguments, | ||
| 131 | DapScopePresentationHint::Registers => dap_types::ScopePresentationHint::Registers, | ||
| 132 | DapScopePresentationHint::ReturnValue => dap_types::ScopePresentationHint::ReturnValue, | ||
| 133 | DapScopePresentationHint::ScopeUnknown => dap_types::ScopePresentationHint::Unknown, | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | impl ProtoConversion for dap_types::SourcePresentationHint { | ||
| 139 | type ProtoType = DapSourcePresentationHint; | ||
| 140 | type Output = Self; | ||
| 141 | |||
| 142 | fn to_proto(&self) -> Self::ProtoType { | ||
| 143 | match self { | ||
| 144 | dap_types::SourcePresentationHint::Normal => DapSourcePresentationHint::SourceNormal, | ||
| 145 | dap_types::SourcePresentationHint::Emphasize => DapSourcePresentationHint::Emphasize, | ||
| 146 | dap_types::SourcePresentationHint::Deemphasize => { | ||
| 147 | DapSourcePresentationHint::Deemphasize | ||
| 148 | } | ||
| 149 | dap_types::SourcePresentationHint::Unknown => DapSourcePresentationHint::SourceUnknown, | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 154 | match payload { | ||
| 155 | DapSourcePresentationHint::SourceNormal => dap_types::SourcePresentationHint::Normal, | ||
| 156 | DapSourcePresentationHint::Emphasize => dap_types::SourcePresentationHint::Emphasize, | ||
| 157 | DapSourcePresentationHint::Deemphasize => { | ||
| 158 | dap_types::SourcePresentationHint::Deemphasize | ||
| 159 | } | ||
| 160 | DapSourcePresentationHint::SourceUnknown => dap_types::SourcePresentationHint::Unknown, | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | impl ProtoConversion for dap_types::Checksum { | ||
| 166 | type ProtoType = DapChecksum; | ||
| 167 | type Output = Self; | ||
| 168 | |||
| 169 | fn to_proto(&self) -> Self::ProtoType { | ||
| 170 | DapChecksum { | ||
| 171 | algorithm: self.algorithm.to_proto().into(), | ||
| 172 | checksum: self.checksum.clone(), | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 177 | Self { | ||
| 178 | algorithm: dap_types::ChecksumAlgorithm::from_proto(payload.algorithm()), | ||
| 179 | checksum: payload.checksum, | ||
| 180 | } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | impl ProtoConversion for dap_types::ChecksumAlgorithm { | ||
| 185 | type ProtoType = DapChecksumAlgorithm; | ||
| 186 | type Output = Self; | ||
| 187 | |||
| 188 | fn to_proto(&self) -> Self::ProtoType { | ||
| 189 | match self { | ||
| 190 | dap_types::ChecksumAlgorithm::Md5 => DapChecksumAlgorithm::Md5, | ||
| 191 | dap_types::ChecksumAlgorithm::Sha1 => DapChecksumAlgorithm::Sha1, | ||
| 192 | dap_types::ChecksumAlgorithm::Sha256 => DapChecksumAlgorithm::Sha256, | ||
| 193 | dap_types::ChecksumAlgorithm::Timestamp => DapChecksumAlgorithm::Timestamp, | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 198 | match payload { | ||
| 199 | DapChecksumAlgorithm::Md5 => dap_types::ChecksumAlgorithm::Md5, | ||
| 200 | DapChecksumAlgorithm::Sha1 => dap_types::ChecksumAlgorithm::Sha1, | ||
| 201 | DapChecksumAlgorithm::Sha256 => dap_types::ChecksumAlgorithm::Sha256, | ||
| 202 | DapChecksumAlgorithm::Timestamp => dap_types::ChecksumAlgorithm::Timestamp, | ||
| 203 | DapChecksumAlgorithm::ChecksumAlgorithmUnspecified => unreachable!(), | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | impl ProtoConversion for dap_types::Source { | ||
| 209 | type ProtoType = DapSource; | ||
| 210 | type Output = Self; | ||
| 211 | |||
| 212 | fn to_proto(&self) -> Self::ProtoType { | ||
| 213 | Self::ProtoType { | ||
| 214 | name: self.name.clone(), | ||
| 215 | path: self.path.clone(), | ||
| 216 | source_reference: self.source_reference, | ||
| 217 | presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()), | ||
| 218 | origin: self.origin.clone(), | ||
| 219 | sources: self | ||
| 220 | .sources | ||
| 221 | .clone() | ||
| 222 | .map(|src| src.to_proto()) | ||
| 223 | .unwrap_or_default(), | ||
| 224 | adapter_data: Default::default(), // TODO Debugger Collab | ||
| 225 | checksums: self | ||
| 226 | .checksums | ||
| 227 | .clone() | ||
| 228 | .map(|c| c.to_proto()) | ||
| 229 | .unwrap_or_default(), | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 233 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 234 | Self { | ||
| 235 | name: payload.name.clone(), | ||
| 236 | path: payload.path.clone(), | ||
| 237 | source_reference: payload.source_reference, | ||
| 238 | presentation_hint: payload | ||
| 239 | .presentation_hint | ||
| 240 | .and_then(DapSourcePresentationHint::from_i32) | ||
| 241 | .map(dap_types::SourcePresentationHint::from_proto), | ||
| 242 | origin: payload.origin.clone(), | ||
| 243 | sources: Some(Vec::<dap_types::Source>::from_proto(payload.sources)), | ||
| 244 | checksums: Some(Vec::<dap_types::Checksum>::from_proto(payload.checksums)), | ||
| 245 | adapter_data: None, // TODO Debugger Collab | ||
| 246 | } | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | impl ProtoConversion for dap_types::StackFrame { | ||
| 251 | type ProtoType = DapStackFrame; | ||
| 252 | type Output = Self; | ||
| 253 | |||
| 254 | fn to_proto(&self) -> Self::ProtoType { | ||
| 255 | Self::ProtoType { | ||
| 256 | id: self.id, | ||
| 257 | name: self.name.clone(), | ||
| 258 | source: self.source.as_ref().map(|src| src.to_proto()), | ||
| 259 | line: self.line, | ||
| 260 | column: self.column, | ||
| 261 | end_line: self.end_line, | ||
| 262 | end_column: self.end_column, | ||
| 263 | can_restart: self.can_restart, | ||
| 264 | instruction_pointer_reference: self.instruction_pointer_reference.clone(), | ||
| 265 | module_id: None, // TODO Debugger Collab | ||
| 266 | presentation_hint: None, // TODO Debugger Collab | ||
| 267 | } | ||
| 268 | } | ||
| 269 | |||
| 270 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 271 | Self { | ||
| 272 | id: payload.id, | ||
| 273 | name: payload.name, | ||
| 274 | source: payload.source.map(dap_types::Source::from_proto), | ||
| 275 | line: payload.line, | ||
| 276 | column: payload.column, | ||
| 277 | end_line: payload.end_line, | ||
| 278 | end_column: payload.end_column, | ||
| 279 | can_restart: payload.can_restart, | ||
| 280 | instruction_pointer_reference: payload.instruction_pointer_reference, | ||
| 281 | module_id: None, // TODO Debugger Collab | ||
| 282 | presentation_hint: None, // TODO Debugger Collab | ||
| 283 | } | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | impl ProtoConversion for dap_types::Module { | ||
| 288 | type ProtoType = DapModule; | ||
| 289 | type Output = Result<Self>; | ||
| 290 | |||
| 291 | fn to_proto(&self) -> Self::ProtoType { | ||
| 292 | let id = match &self.id { | ||
| 293 | dap_types::ModuleId::Number(num) => proto::dap_module_id::Id::Number(*num), | ||
| 294 | dap_types::ModuleId::String(string) => proto::dap_module_id::Id::String(string.clone()), | ||
| 295 | }; | ||
| 296 | |||
| 297 | DapModule { | ||
| 298 | id: Some(proto::DapModuleId { id: Some(id) }), | ||
| 299 | name: self.name.clone(), | ||
| 300 | path: self.path.clone(), | ||
| 301 | is_optimized: self.is_optimized, | ||
| 302 | is_user_code: self.is_user_code, | ||
| 303 | version: self.version.clone(), | ||
| 304 | symbol_status: self.symbol_status.clone(), | ||
| 305 | symbol_file_path: self.symbol_file_path.clone(), | ||
| 306 | date_time_stamp: self.date_time_stamp.clone(), | ||
| 307 | address_range: self.address_range.clone(), | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | fn from_proto(payload: Self::ProtoType) -> Result<Self> { | ||
| 312 | let id = match payload | ||
| 313 | .id | ||
| 314 | .ok_or(anyhow!("All DapModule proto messages must have an id"))? | ||
| 315 | .id | ||
| 316 | .ok_or(anyhow!("All DapModuleID proto messages must have an id"))? | ||
| 317 | { | ||
| 318 | proto::dap_module_id::Id::String(string) => dap_types::ModuleId::String(string), | ||
| 319 | proto::dap_module_id::Id::Number(num) => dap_types::ModuleId::Number(num), | ||
| 320 | }; | ||
| 321 | |||
| 322 | Ok(Self { | ||
| 323 | id, | ||
| 324 | name: payload.name, | ||
| 325 | path: payload.path, | ||
| 326 | is_optimized: payload.is_optimized, | ||
| 327 | is_user_code: payload.is_user_code, | ||
| 328 | version: payload.version, | ||
| 329 | symbol_status: payload.symbol_status, | ||
| 330 | symbol_file_path: payload.symbol_file_path, | ||
| 331 | date_time_stamp: payload.date_time_stamp, | ||
| 332 | address_range: payload.address_range, | ||
| 333 | }) | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | impl ProtoConversion for dap_types::SteppingGranularity { | ||
| 338 | type ProtoType = proto::SteppingGranularity; | ||
| 339 | type Output = Self; | ||
| 340 | |||
| 341 | fn to_proto(&self) -> Self::ProtoType { | ||
| 342 | match self { | ||
| 343 | dap_types::SteppingGranularity::Statement => proto::SteppingGranularity::Statement, | ||
| 344 | dap_types::SteppingGranularity::Line => proto::SteppingGranularity::Line, | ||
| 345 | dap_types::SteppingGranularity::Instruction => proto::SteppingGranularity::Instruction, | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 350 | match payload { | ||
| 351 | proto::SteppingGranularity::Line => dap_types::SteppingGranularity::Line, | ||
| 352 | proto::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction, | ||
| 353 | proto::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement, | ||
| 354 | } | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | impl ProtoConversion for dap_types::OutputEventCategory { | ||
| 359 | type ProtoType = proto::DapOutputCategory; | ||
| 360 | type Output = Self; | ||
| 361 | |||
| 362 | fn to_proto(&self) -> Self::ProtoType { | ||
| 363 | match self { | ||
| 364 | Self::Console => proto::DapOutputCategory::ConsoleOutput, | ||
| 365 | Self::Important => proto::DapOutputCategory::Important, | ||
| 366 | Self::Stdout => proto::DapOutputCategory::Stdout, | ||
| 367 | Self::Stderr => proto::DapOutputCategory::Stderr, | ||
| 368 | _ => proto::DapOutputCategory::Unknown, | ||
| 369 | } | ||
| 370 | } | ||
| 371 | |||
| 372 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 373 | match payload { | ||
| 374 | proto::DapOutputCategory::ConsoleOutput => Self::Console, | ||
| 375 | proto::DapOutputCategory::Important => Self::Important, | ||
| 376 | proto::DapOutputCategory::Stdout => Self::Stdout, | ||
| 377 | proto::DapOutputCategory::Stderr => Self::Stderr, | ||
| 378 | proto::DapOutputCategory::Unknown => Self::Unknown, | ||
| 379 | } | ||
| 380 | } | ||
| 381 | } | ||
| 382 | |||
| 383 | impl ProtoConversion for dap_types::OutputEvent { | ||
| 384 | type ProtoType = proto::DapOutputEvent; | ||
| 385 | type Output = Self; | ||
| 386 | |||
| 387 | fn to_proto(&self) -> Self::ProtoType { | ||
| 388 | proto::DapOutputEvent { | ||
| 389 | category: self | ||
| 390 | .category | ||
| 391 | .as_ref() | ||
| 392 | .map(|category| category.to_proto().into()), | ||
| 393 | output: self.output.clone(), | ||
| 394 | variables_reference: self.variables_reference, | ||
| 395 | source: self.source.as_ref().map(|source| source.to_proto()), | ||
| 396 | line: self.line.map(|line| line as u32), | ||
| 397 | column: self.column.map(|column| column as u32), | ||
| 398 | group: self.group.map(|group| group.to_proto().into()), | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 403 | dap_types::OutputEvent { | ||
| 404 | category: payload | ||
| 405 | .category | ||
| 406 | .and_then(proto::DapOutputCategory::from_i32) | ||
| 407 | .map(OutputEventCategory::from_proto), | ||
| 408 | output: payload.output.clone(), | ||
| 409 | variables_reference: payload.variables_reference, | ||
| 410 | source: payload.source.map(Source::from_proto), | ||
| 411 | line: payload.line.map(|line| line as u64), | ||
| 412 | column: payload.column.map(|column| column as u64), | ||
| 413 | group: payload | ||
| 414 | .group | ||
| 415 | .and_then(proto::DapOutputEventGroup::from_i32) | ||
| 416 | .map(OutputEventGroup::from_proto), | ||
| 417 | data: None, | ||
| 418 | location_reference: None, | ||
| 419 | } | ||
| 420 | } | ||
| 421 | } | ||
| 422 | |||
| 423 | impl ProtoConversion for dap_types::OutputEventGroup { | ||
| 424 | type ProtoType = proto::DapOutputEventGroup; | ||
| 425 | type Output = Self; | ||
| 426 | |||
| 427 | fn to_proto(&self) -> Self::ProtoType { | ||
| 428 | match self { | ||
| 429 | dap_types::OutputEventGroup::Start => proto::DapOutputEventGroup::Start, | ||
| 430 | dap_types::OutputEventGroup::StartCollapsed => { | ||
| 431 | proto::DapOutputEventGroup::StartCollapsed | ||
| 432 | } | ||
| 433 | dap_types::OutputEventGroup::End => proto::DapOutputEventGroup::End, | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 438 | match payload { | ||
| 439 | proto::DapOutputEventGroup::Start => Self::Start, | ||
| 440 | proto::DapOutputEventGroup::StartCollapsed => Self::StartCollapsed, | ||
| 441 | proto::DapOutputEventGroup::End => Self::End, | ||
| 442 | } | ||
| 443 | } | ||
| 444 | } | ||
| 445 | |||
| 446 | impl ProtoConversion for dap_types::CompletionItem { | ||
| 447 | type ProtoType = proto::DapCompletionItem; | ||
| 448 | type Output = Self; | ||
| 449 | |||
| 450 | fn to_proto(&self) -> Self::ProtoType { | ||
| 451 | proto::DapCompletionItem { | ||
| 452 | label: self.label.clone(), | ||
| 453 | text: self.text.clone(), | ||
| 454 | detail: self.detail.clone(), | ||
| 455 | typ: self | ||
| 456 | .type_ | ||
| 457 | .as_ref() | ||
| 458 | .map(ProtoConversion::to_proto) | ||
| 459 | .map(|typ| typ.into()), | ||
| 460 | start: self.start, | ||
| 461 | length: self.length, | ||
| 462 | selection_start: self.selection_start, | ||
| 463 | selection_length: self.selection_length, | ||
| 464 | sort_text: self.sort_text.clone(), | ||
| 465 | } | ||
| 466 | } | ||
| 467 | |||
| 468 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 469 | let typ = payload.typ(); // todo(debugger): This might be a potential issue/bug because it defaults to a type when it's None | ||
| 470 | |||
| 471 | Self { | ||
| 472 | label: payload.label, | ||
| 473 | detail: payload.detail, | ||
| 474 | sort_text: payload.sort_text, | ||
| 475 | text: payload.text.clone(), | ||
| 476 | type_: Some(dap_types::CompletionItemType::from_proto(typ)), | ||
| 477 | start: payload.start, | ||
| 478 | length: payload.length, | ||
| 479 | selection_start: payload.selection_start, | ||
| 480 | selection_length: payload.selection_length, | ||
| 481 | } | ||
| 482 | } | ||
| 483 | } | ||
| 484 | |||
| 485 | impl ProtoConversion for dap_types::EvaluateArgumentsContext { | ||
| 486 | type ProtoType = DapEvaluateContext; | ||
| 487 | type Output = Self; | ||
| 488 | |||
| 489 | fn to_proto(&self) -> Self::ProtoType { | ||
| 490 | match self { | ||
| 491 | dap_types::EvaluateArgumentsContext::Variables => { | ||
| 492 | proto::DapEvaluateContext::EvaluateVariables | ||
| 493 | } | ||
| 494 | dap_types::EvaluateArgumentsContext::Watch => proto::DapEvaluateContext::Watch, | ||
| 495 | dap_types::EvaluateArgumentsContext::Hover => proto::DapEvaluateContext::Hover, | ||
| 496 | dap_types::EvaluateArgumentsContext::Repl => proto::DapEvaluateContext::Repl, | ||
| 497 | dap_types::EvaluateArgumentsContext::Clipboard => proto::DapEvaluateContext::Clipboard, | ||
| 498 | dap_types::EvaluateArgumentsContext::Unknown => { | ||
| 499 | proto::DapEvaluateContext::EvaluateUnknown | ||
| 500 | } | ||
| 501 | _ => proto::DapEvaluateContext::EvaluateUnknown, | ||
| 502 | } | ||
| 503 | } | ||
| 504 | |||
| 505 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 506 | match payload { | ||
| 507 | proto::DapEvaluateContext::EvaluateVariables => { | ||
| 508 | dap_types::EvaluateArgumentsContext::Variables | ||
| 509 | } | ||
| 510 | proto::DapEvaluateContext::Watch => dap_types::EvaluateArgumentsContext::Watch, | ||
| 511 | proto::DapEvaluateContext::Hover => dap_types::EvaluateArgumentsContext::Hover, | ||
| 512 | proto::DapEvaluateContext::Repl => dap_types::EvaluateArgumentsContext::Repl, | ||
| 513 | proto::DapEvaluateContext::Clipboard => dap_types::EvaluateArgumentsContext::Clipboard, | ||
| 514 | proto::DapEvaluateContext::EvaluateUnknown => { | ||
| 515 | dap_types::EvaluateArgumentsContext::Unknown | ||
| 516 | } | ||
| 517 | } | ||
| 518 | } | ||
| 519 | } | ||
| 520 | |||
| 521 | impl ProtoConversion for dap_types::CompletionItemType { | ||
| 522 | type ProtoType = proto::DapCompletionItemType; | ||
| 523 | type Output = Self; | ||
| 524 | |||
| 525 | fn to_proto(&self) -> Self::ProtoType { | ||
| 526 | match self { | ||
| 527 | dap_types::CompletionItemType::Class => proto::DapCompletionItemType::Class, | ||
| 528 | dap_types::CompletionItemType::Color => proto::DapCompletionItemType::Color, | ||
| 529 | dap_types::CompletionItemType::Constructor => proto::DapCompletionItemType::Constructor, | ||
| 530 | dap_types::CompletionItemType::Customcolor => proto::DapCompletionItemType::Customcolor, | ||
| 531 | dap_types::CompletionItemType::Enum => proto::DapCompletionItemType::Enum, | ||
| 532 | dap_types::CompletionItemType::Field => proto::DapCompletionItemType::Field, | ||
| 533 | dap_types::CompletionItemType::File => proto::DapCompletionItemType::CompletionItemFile, | ||
| 534 | dap_types::CompletionItemType::Function => proto::DapCompletionItemType::Function, | ||
| 535 | dap_types::CompletionItemType::Interface => proto::DapCompletionItemType::Interface, | ||
| 536 | dap_types::CompletionItemType::Keyword => proto::DapCompletionItemType::Keyword, | ||
| 537 | dap_types::CompletionItemType::Method => proto::DapCompletionItemType::Method, | ||
| 538 | dap_types::CompletionItemType::Module => proto::DapCompletionItemType::Module, | ||
| 539 | dap_types::CompletionItemType::Property => proto::DapCompletionItemType::Property, | ||
| 540 | dap_types::CompletionItemType::Reference => proto::DapCompletionItemType::Reference, | ||
| 541 | dap_types::CompletionItemType::Snippet => proto::DapCompletionItemType::Snippet, | ||
| 542 | dap_types::CompletionItemType::Text => proto::DapCompletionItemType::Text, | ||
| 543 | dap_types::CompletionItemType::Unit => proto::DapCompletionItemType::Unit, | ||
| 544 | dap_types::CompletionItemType::Value => proto::DapCompletionItemType::Value, | ||
| 545 | dap_types::CompletionItemType::Variable => proto::DapCompletionItemType::Variable, | ||
| 546 | } | ||
| 547 | } | ||
| 548 | |||
| 549 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 550 | match payload { | ||
| 551 | proto::DapCompletionItemType::Class => dap_types::CompletionItemType::Class, | ||
| 552 | proto::DapCompletionItemType::Color => dap_types::CompletionItemType::Color, | ||
| 553 | proto::DapCompletionItemType::CompletionItemFile => dap_types::CompletionItemType::File, | ||
| 554 | proto::DapCompletionItemType::Constructor => dap_types::CompletionItemType::Constructor, | ||
| 555 | proto::DapCompletionItemType::Customcolor => dap_types::CompletionItemType::Customcolor, | ||
| 556 | proto::DapCompletionItemType::Enum => dap_types::CompletionItemType::Enum, | ||
| 557 | proto::DapCompletionItemType::Field => dap_types::CompletionItemType::Field, | ||
| 558 | proto::DapCompletionItemType::Function => dap_types::CompletionItemType::Function, | ||
| 559 | proto::DapCompletionItemType::Interface => dap_types::CompletionItemType::Interface, | ||
| 560 | proto::DapCompletionItemType::Keyword => dap_types::CompletionItemType::Keyword, | ||
| 561 | proto::DapCompletionItemType::Method => dap_types::CompletionItemType::Method, | ||
| 562 | proto::DapCompletionItemType::Module => dap_types::CompletionItemType::Module, | ||
| 563 | proto::DapCompletionItemType::Property => dap_types::CompletionItemType::Property, | ||
| 564 | proto::DapCompletionItemType::Reference => dap_types::CompletionItemType::Reference, | ||
| 565 | proto::DapCompletionItemType::Snippet => dap_types::CompletionItemType::Snippet, | ||
| 566 | proto::DapCompletionItemType::Text => dap_types::CompletionItemType::Text, | ||
| 567 | proto::DapCompletionItemType::Unit => dap_types::CompletionItemType::Unit, | ||
| 568 | proto::DapCompletionItemType::Value => dap_types::CompletionItemType::Value, | ||
| 569 | proto::DapCompletionItemType::Variable => dap_types::CompletionItemType::Variable, | ||
| 570 | } | ||
| 571 | } | ||
| 572 | } | ||
| 573 | |||
| 574 | impl ProtoConversion for dap_types::Thread { | ||
| 575 | type ProtoType = proto::DapThread; | ||
| 576 | type Output = Self; | ||
| 577 | |||
| 578 | fn to_proto(&self) -> Self::ProtoType { | ||
| 579 | proto::DapThread { | ||
| 580 | id: self.id, | ||
| 581 | name: self.name.clone(), | ||
| 582 | } | ||
| 583 | } | ||
| 584 | |||
| 585 | fn from_proto(payload: Self::ProtoType) -> Self { | ||
| 586 | Self { | ||
| 587 | id: payload.id, | ||
| 588 | name: payload.name, | ||
| 589 | } | ||
| 590 | } | ||
| 591 | } | ||
| @@ -0,0 +1,891 @@ | |||
| 1 | use anyhow::{anyhow, bail, Context, Result}; | ||
| 2 | use dap_types::{ | ||
| 3 | messages::{Message, Response}, | ||
| 4 | ErrorResponse, | ||
| 5 | }; | ||
| 6 | use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; | ||
| 7 | use gpui::AsyncApp; | ||
| 8 | use settings::Settings as _; | ||
| 9 | use smallvec::SmallVec; | ||
| 10 | use smol::{ | ||
| 11 | channel::{unbounded, Receiver, Sender}, | ||
| 12 | io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, | ||
| 13 | lock::Mutex, | ||
| 14 | net::{TcpListener, TcpStream}, | ||
| 15 | process::Child, | ||
| 16 | }; | ||
| 17 | use std::{ | ||
| 18 | collections::HashMap, | ||
| 19 | net::{Ipv4Addr, SocketAddrV4}, | ||
| 20 | process::Stdio, | ||
| 21 | sync::Arc, | ||
| 22 | time::Duration, | ||
| 23 | }; | ||
| 24 | use task::TCPHost; | ||
| 25 | use util::ResultExt as _; | ||
| 26 | |||
| 27 | use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; | ||
| 28 | |||
| 29 | pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>; | ||
| 30 | |||
| 31 | #[derive(PartialEq, Eq, Clone, Copy)] | ||
| 32 | pub enum LogKind { | ||
| 33 | Adapter, | ||
| 34 | Rpc, | ||
| 35 | } | ||
| 36 | |||
| 37 | pub enum IoKind { | ||
| 38 | StdIn, | ||
| 39 | StdOut, | ||
| 40 | StdErr, | ||
| 41 | } | ||
| 42 | |||
| 43 | pub struct TransportPipe { | ||
| 44 | input: Box<dyn AsyncWrite + Unpin + Send + 'static>, | ||
| 45 | output: Box<dyn AsyncRead + Unpin + Send + 'static>, | ||
| 46 | stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>, | ||
| 47 | stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>, | ||
| 48 | } | ||
| 49 | |||
| 50 | impl TransportPipe { | ||
| 51 | pub fn new( | ||
| 52 | input: Box<dyn AsyncWrite + Unpin + Send + 'static>, | ||
| 53 | output: Box<dyn AsyncRead + Unpin + Send + 'static>, | ||
| 54 | stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>, | ||
| 55 | stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>, | ||
| 56 | ) -> Self { | ||
| 57 | TransportPipe { | ||
| 58 | input, | ||
| 59 | output, | ||
| 60 | stdout, | ||
| 61 | stderr, | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>; | ||
| 67 | type LogHandlers = Arc<parking_lot::Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>; | ||
| 68 | |||
| 69 | pub enum Transport { | ||
| 70 | Stdio(StdioTransport), | ||
| 71 | Tcp(TcpTransport), | ||
| 72 | #[cfg(any(test, feature = "test-support"))] | ||
| 73 | Fake(FakeTransport), | ||
| 74 | } | ||
| 75 | |||
| 76 | impl Transport { | ||
| 77 | #[cfg(any(test, feature = "test-support"))] | ||
| 78 | async fn start(_: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { | ||
| 79 | #[cfg(any(test, feature = "test-support"))] | ||
| 80 | return FakeTransport::start(cx) | ||
| 81 | .await | ||
| 82 | .map(|(transports, fake)| (transports, Self::Fake(fake))); | ||
| 83 | } | ||
| 84 | |||
| 85 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 86 | async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { | ||
| 87 | if binary.connection.is_some() { | ||
| 88 | TcpTransport::start(binary, cx) | ||
| 89 | .await | ||
| 90 | .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) | ||
| 91 | } else { | ||
| 92 | StdioTransport::start(binary, cx) | ||
| 93 | .await | ||
| 94 | .map(|(transports, stdio)| (transports, Self::Stdio(stdio))) | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | fn has_adapter_logs(&self) -> bool { | ||
| 99 | match self { | ||
| 100 | Transport::Stdio(stdio_transport) => stdio_transport.has_adapter_logs(), | ||
| 101 | Transport::Tcp(tcp_transport) => tcp_transport.has_adapter_logs(), | ||
| 102 | #[cfg(any(test, feature = "test-support"))] | ||
| 103 | Transport::Fake(fake_transport) => fake_transport.has_adapter_logs(), | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | async fn kill(&self) -> Result<()> { | ||
| 108 | match self { | ||
| 109 | Transport::Stdio(stdio_transport) => stdio_transport.kill().await, | ||
| 110 | Transport::Tcp(tcp_transport) => tcp_transport.kill().await, | ||
| 111 | #[cfg(any(test, feature = "test-support"))] | ||
| 112 | Transport::Fake(fake_transport) => fake_transport.kill().await, | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | #[cfg(any(test, feature = "test-support"))] | ||
| 117 | pub(crate) fn as_fake(&self) -> &FakeTransport { | ||
| 118 | match self { | ||
| 119 | Transport::Fake(fake_transport) => fake_transport, | ||
| 120 | _ => panic!("Not a fake transport layer"), | ||
| 121 | } | ||
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | pub(crate) struct TransportDelegate { | ||
| 126 | log_handlers: LogHandlers, | ||
| 127 | current_requests: Requests, | ||
| 128 | pending_requests: Requests, | ||
| 129 | transport: Transport, | ||
| 130 | server_tx: Arc<Mutex<Option<Sender<Message>>>>, | ||
| 131 | } | ||
| 132 | |||
| 133 | impl TransportDelegate { | ||
| 134 | pub(crate) async fn start( | ||
| 135 | binary: &DebugAdapterBinary, | ||
| 136 | cx: AsyncApp, | ||
| 137 | ) -> Result<((Receiver<Message>, Sender<Message>), Self)> { | ||
| 138 | let (transport_pipes, transport) = Transport::start(binary, cx.clone()).await?; | ||
| 139 | let mut this = Self { | ||
| 140 | transport, | ||
| 141 | server_tx: Default::default(), | ||
| 142 | log_handlers: Default::default(), | ||
| 143 | current_requests: Default::default(), | ||
| 144 | pending_requests: Default::default(), | ||
| 145 | }; | ||
| 146 | let messages = this.start_handlers(transport_pipes, cx).await?; | ||
| 147 | Ok((messages, this)) | ||
| 148 | } | ||
| 149 | |||
| 150 | async fn start_handlers( | ||
| 151 | &mut self, | ||
| 152 | mut params: TransportPipe, | ||
| 153 | cx: AsyncApp, | ||
| 154 | ) -> Result<(Receiver<Message>, Sender<Message>)> { | ||
| 155 | let (client_tx, server_rx) = unbounded::<Message>(); | ||
| 156 | let (server_tx, client_rx) = unbounded::<Message>(); | ||
| 157 | |||
| 158 | let log_dap_communications = | ||
| 159 | cx.update(|cx| DebuggerSettings::get_global(cx).log_dap_communications) | ||
| 160 | .with_context(|| "Failed to get Debugger Setting log dap communications error in transport::start_handlers. Defaulting to false") | ||
| 161 | .unwrap_or(false); | ||
| 162 | |||
| 163 | let log_handler = if log_dap_communications { | ||
| 164 | Some(self.log_handlers.clone()) | ||
| 165 | } else { | ||
| 166 | None | ||
| 167 | }; | ||
| 168 | |||
| 169 | cx.update(|cx| { | ||
| 170 | if let Some(stdout) = params.stdout.take() { | ||
| 171 | cx.background_executor() | ||
| 172 | .spawn(Self::handle_adapter_log(stdout, log_handler.clone())) | ||
| 173 | .detach_and_log_err(cx); | ||
| 174 | } | ||
| 175 | |||
| 176 | cx.background_executor() | ||
| 177 | .spawn(Self::handle_output( | ||
| 178 | params.output, | ||
| 179 | client_tx, | ||
| 180 | self.pending_requests.clone(), | ||
| 181 | log_handler.clone(), | ||
| 182 | )) | ||
| 183 | .detach_and_log_err(cx); | ||
| 184 | |||
| 185 | if let Some(stderr) = params.stderr.take() { | ||
| 186 | cx.background_executor() | ||
| 187 | .spawn(Self::handle_error(stderr, self.log_handlers.clone())) | ||
| 188 | .detach_and_log_err(cx); | ||
| 189 | } | ||
| 190 | |||
| 191 | cx.background_executor() | ||
| 192 | .spawn(Self::handle_input( | ||
| 193 | params.input, | ||
| 194 | client_rx, | ||
| 195 | self.current_requests.clone(), | ||
| 196 | self.pending_requests.clone(), | ||
| 197 | log_handler.clone(), | ||
| 198 | )) | ||
| 199 | .detach_and_log_err(cx); | ||
| 200 | })?; | ||
| 201 | |||
| 202 | { | ||
| 203 | let mut lock = self.server_tx.lock().await; | ||
| 204 | *lock = Some(server_tx.clone()); | ||
| 205 | } | ||
| 206 | |||
| 207 | Ok((server_rx, server_tx)) | ||
| 208 | } | ||
| 209 | |||
| 210 | pub(crate) async fn add_pending_request( | ||
| 211 | &self, | ||
| 212 | sequence_id: u64, | ||
| 213 | request: oneshot::Sender<Result<Response>>, | ||
| 214 | ) { | ||
| 215 | let mut pending_requests = self.pending_requests.lock().await; | ||
| 216 | pending_requests.insert(sequence_id, request); | ||
| 217 | } | ||
| 218 | |||
| 219 | pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) { | ||
| 220 | let mut pending_requests = self.pending_requests.lock().await; | ||
| 221 | pending_requests.remove(sequence_id); | ||
| 222 | } | ||
| 223 | |||
| 224 | pub(crate) async fn send_message(&self, message: Message) -> Result<()> { | ||
| 225 | if let Some(server_tx) = self.server_tx.lock().await.as_ref() { | ||
| 226 | server_tx | ||
| 227 | .send(message) | ||
| 228 | .await | ||
| 229 | .map_err(|e| anyhow!("Failed to send message: {}", e)) | ||
| 230 | } else { | ||
| 231 | Err(anyhow!("Server tx already dropped")) | ||
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 | async fn handle_adapter_log<Stdout>( | ||
| 236 | stdout: Stdout, | ||
| 237 | log_handlers: Option<LogHandlers>, | ||
| 238 | ) -> Result<()> | ||
| 239 | where | ||
| 240 | Stdout: AsyncRead + Unpin + Send + 'static, | ||
| 241 | { | ||
| 242 | let mut reader = BufReader::new(stdout); | ||
| 243 | let mut line = String::new(); | ||
| 244 | |||
| 245 | let result = loop { | ||
| 246 | line.truncate(0); | ||
| 247 | |||
| 248 | let bytes_read = match reader.read_line(&mut line).await { | ||
| 249 | Ok(bytes_read) => bytes_read, | ||
| 250 | Err(e) => break Err(e.into()), | ||
| 251 | }; | ||
| 252 | |||
| 253 | if bytes_read == 0 { | ||
| 254 | break Err(anyhow!("Debugger log stream closed")); | ||
| 255 | } | ||
| 256 | |||
| 257 | if let Some(log_handlers) = log_handlers.as_ref() { | ||
| 258 | for (kind, handler) in log_handlers.lock().iter_mut() { | ||
| 259 | if matches!(kind, LogKind::Adapter) { | ||
| 260 | handler(IoKind::StdOut, line.as_str()); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 265 | smol::future::yield_now().await; | ||
| 266 | }; | ||
| 267 | |||
| 268 | log::debug!("Handle adapter log dropped"); | ||
| 269 | |||
| 270 | result | ||
| 271 | } | ||
| 272 | |||
| 273 | fn build_rpc_message(message: String) -> String { | ||
| 274 | format!("Content-Length: {}\r\n\r\n{}", message.len(), message) | ||
| 275 | } | ||
| 276 | |||
| 277 | async fn handle_input<Stdin>( | ||
| 278 | mut server_stdin: Stdin, | ||
| 279 | client_rx: Receiver<Message>, | ||
| 280 | current_requests: Requests, | ||
| 281 | pending_requests: Requests, | ||
| 282 | log_handlers: Option<LogHandlers>, | ||
| 283 | ) -> Result<()> | ||
| 284 | where | ||
| 285 | Stdin: AsyncWrite + Unpin + Send + 'static, | ||
| 286 | { | ||
| 287 | let result = loop { | ||
| 288 | match client_rx.recv().await { | ||
| 289 | Ok(message) => { | ||
| 290 | if let Message::Request(request) = &message { | ||
| 291 | if let Some(sender) = current_requests.lock().await.remove(&request.seq) { | ||
| 292 | pending_requests.lock().await.insert(request.seq, sender); | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | let message = match serde_json::to_string(&message) { | ||
| 297 | Ok(message) => message, | ||
| 298 | Err(e) => break Err(e.into()), | ||
| 299 | }; | ||
| 300 | |||
| 301 | if let Some(log_handlers) = log_handlers.as_ref() { | ||
| 302 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | ||
| 303 | if matches!(kind, LogKind::Rpc) { | ||
| 304 | log_handler(IoKind::StdIn, &message); | ||
| 305 | } | ||
| 306 | } | ||
| 307 | } | ||
| 308 | |||
| 309 | if let Err(e) = server_stdin | ||
| 310 | .write_all(Self::build_rpc_message(message).as_bytes()) | ||
| 311 | .await | ||
| 312 | { | ||
| 313 | break Err(e.into()); | ||
| 314 | } | ||
| 315 | |||
| 316 | if let Err(e) = server_stdin.flush().await { | ||
| 317 | break Err(e.into()); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | Err(error) => break Err(error.into()), | ||
| 321 | } | ||
| 322 | |||
| 323 | smol::future::yield_now().await; | ||
| 324 | }; | ||
| 325 | |||
| 326 | log::debug!("Handle adapter input dropped"); | ||
| 327 | |||
| 328 | result | ||
| 329 | } | ||
| 330 | |||
| 331 | async fn handle_output<Stdout>( | ||
| 332 | server_stdout: Stdout, | ||
| 333 | client_tx: Sender<Message>, | ||
| 334 | pending_requests: Requests, | ||
| 335 | log_handlers: Option<LogHandlers>, | ||
| 336 | ) -> Result<()> | ||
| 337 | where | ||
| 338 | Stdout: AsyncRead + Unpin + Send + 'static, | ||
| 339 | { | ||
| 340 | let mut recv_buffer = String::new(); | ||
| 341 | let mut reader = BufReader::new(server_stdout); | ||
| 342 | |||
| 343 | let result = loop { | ||
| 344 | let message = | ||
| 345 | Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref()) | ||
| 346 | .await; | ||
| 347 | |||
| 348 | match message { | ||
| 349 | Ok(Message::Response(res)) => { | ||
| 350 | if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { | ||
| 351 | if let Err(e) = tx.send(Self::process_response(res)) { | ||
| 352 | log::trace!("Did not send response `{:?}` for a cancelled", e); | ||
| 353 | } | ||
| 354 | } else { | ||
| 355 | client_tx.send(Message::Response(res)).await?; | ||
| 356 | }; | ||
| 357 | } | ||
| 358 | Ok(message) => { | ||
| 359 | client_tx.send(message).await?; | ||
| 360 | } | ||
| 361 | Err(e) => break Err(e), | ||
| 362 | } | ||
| 363 | |||
| 364 | smol::future::yield_now().await; | ||
| 365 | }; | ||
| 366 | |||
| 367 | drop(client_tx); | ||
| 368 | |||
| 369 | log::debug!("Handle adapter output dropped"); | ||
| 370 | |||
| 371 | result | ||
| 372 | } | ||
| 373 | |||
| 374 | async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()> | ||
| 375 | where | ||
| 376 | Stderr: AsyncRead + Unpin + Send + 'static, | ||
| 377 | { | ||
| 378 | let mut buffer = String::new(); | ||
| 379 | |||
| 380 | let mut reader = BufReader::new(stderr); | ||
| 381 | |||
| 382 | let result = loop { | ||
| 383 | match reader.read_line(&mut buffer).await { | ||
| 384 | Ok(0) => break Err(anyhow!("debugger error stream closed")), | ||
| 385 | Ok(_) => { | ||
| 386 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | ||
| 387 | if matches!(kind, LogKind::Adapter) { | ||
| 388 | log_handler(IoKind::StdErr, buffer.as_str()); | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 392 | buffer.truncate(0); | ||
| 393 | } | ||
| 394 | Err(error) => break Err(error.into()), | ||
| 395 | } | ||
| 396 | |||
| 397 | smol::future::yield_now().await; | ||
| 398 | }; | ||
| 399 | |||
| 400 | log::debug!("Handle adapter error dropped"); | ||
| 401 | |||
| 402 | result | ||
| 403 | } | ||
| 404 | |||
| 405 | fn process_response(response: Response) -> Result<Response> { | ||
| 406 | if response.success { | ||
| 407 | Ok(response) | ||
| 408 | } else { | ||
| 409 | if let Some(error_message) = response | ||
| 410 | .body | ||
| 411 | .clone() | ||
| 412 | .and_then(|body| serde_json::from_value::<ErrorResponse>(body).ok()) | ||
| 413 | .and_then(|response| response.error.map(|msg| msg.format)) | ||
| 414 | .or_else(|| response.message.clone()) | ||
| 415 | { | ||
| 416 | return Err(anyhow!(error_message)); | ||
| 417 | }; | ||
| 418 | |||
| 419 | Err(anyhow!( | ||
| 420 | "Received error response from adapter. Response: {:?}", | ||
| 421 | response.clone() | ||
| 422 | )) | ||
| 423 | } | ||
| 424 | } | ||
| 425 | |||
| 426 | async fn receive_server_message<Stdout>( | ||
| 427 | reader: &mut BufReader<Stdout>, | ||
| 428 | buffer: &mut String, | ||
| 429 | log_handlers: Option<&LogHandlers>, | ||
| 430 | ) -> Result<Message> | ||
| 431 | where | ||
| 432 | Stdout: AsyncRead + Unpin + Send + 'static, | ||
| 433 | { | ||
| 434 | let mut content_length = None; | ||
| 435 | loop { | ||
| 436 | buffer.truncate(0); | ||
| 437 | |||
| 438 | if reader | ||
| 439 | .read_line(buffer) | ||
| 440 | .await | ||
| 441 | .with_context(|| "reading a message from server")? | ||
| 442 | == 0 | ||
| 443 | { | ||
| 444 | return Err(anyhow!("debugger reader stream closed")); | ||
| 445 | }; | ||
| 446 | |||
| 447 | if buffer == "\r\n" { | ||
| 448 | break; | ||
| 449 | } | ||
| 450 | |||
| 451 | let parts = buffer.trim().split_once(": "); | ||
| 452 | |||
| 453 | match parts { | ||
| 454 | Some(("Content-Length", value)) => { | ||
| 455 | content_length = Some(value.parse().context("invalid content length")?); | ||
| 456 | } | ||
| 457 | _ => {} | ||
| 458 | } | ||
| 459 | } | ||
| 460 | |||
| 461 | let content_length = content_length.context("missing content length")?; | ||
| 462 | |||
| 463 | let mut content = vec![0; content_length]; | ||
| 464 | reader | ||
| 465 | .read_exact(&mut content) | ||
| 466 | .await | ||
| 467 | .with_context(|| "reading after a loop")?; | ||
| 468 | |||
| 469 | let message = std::str::from_utf8(&content).context("invalid utf8 from server")?; | ||
| 470 | |||
| 471 | if let Some(log_handlers) = log_handlers { | ||
| 472 | for (kind, log_handler) in log_handlers.lock().iter_mut() { | ||
| 473 | if matches!(kind, LogKind::Rpc) { | ||
| 474 | log_handler(IoKind::StdOut, &message); | ||
| 475 | } | ||
| 476 | } | ||
| 477 | } | ||
| 478 | |||
| 479 | Ok(serde_json::from_str::<Message>(message)?) | ||
| 480 | } | ||
| 481 | |||
| 482 | pub async fn shutdown(&self) -> Result<()> { | ||
| 483 | log::debug!("Start shutdown client"); | ||
| 484 | |||
| 485 | if let Some(server_tx) = self.server_tx.lock().await.take().as_ref() { | ||
| 486 | server_tx.close(); | ||
| 487 | } | ||
| 488 | |||
| 489 | let mut current_requests = self.current_requests.lock().await; | ||
| 490 | let mut pending_requests = self.pending_requests.lock().await; | ||
| 491 | |||
| 492 | current_requests.clear(); | ||
| 493 | pending_requests.clear(); | ||
| 494 | |||
| 495 | let _ = self.transport.kill().await.log_err(); | ||
| 496 | |||
| 497 | drop(current_requests); | ||
| 498 | drop(pending_requests); | ||
| 499 | |||
| 500 | log::debug!("Shutdown client completed"); | ||
| 501 | |||
| 502 | anyhow::Ok(()) | ||
| 503 | } | ||
| 504 | |||
| 505 | pub fn has_adapter_logs(&self) -> bool { | ||
| 506 | self.transport.has_adapter_logs() | ||
| 507 | } | ||
| 508 | |||
| 509 | pub fn transport(&self) -> &Transport { | ||
| 510 | &self.transport | ||
| 511 | } | ||
| 512 | |||
| 513 | pub fn add_log_handler<F>(&self, f: F, kind: LogKind) | ||
| 514 | where | ||
| 515 | F: 'static + Send + FnMut(IoKind, &str), | ||
| 516 | { | ||
| 517 | let mut log_handlers = self.log_handlers.lock(); | ||
| 518 | log_handlers.push((kind, Box::new(f))); | ||
| 519 | } | ||
| 520 | } | ||
| 521 | |||
| 522 | pub struct TcpTransport { | ||
| 523 | pub port: u16, | ||
| 524 | pub host: Ipv4Addr, | ||
| 525 | pub timeout: u64, | ||
| 526 | process: Mutex<Child>, | ||
| 527 | } | ||
| 528 | |||
| 529 | impl TcpTransport { | ||
| 530 | /// Get an open port to use with the tcp client when not supplied by debug config | ||
| 531 | pub async fn port(host: &TCPHost) -> Result<u16> { | ||
| 532 | if let Some(port) = host.port { | ||
| 533 | Ok(port) | ||
| 534 | } else { | ||
| 535 | Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0)) | ||
| 536 | .await? | ||
| 537 | .local_addr()? | ||
| 538 | .port()) | ||
| 539 | } | ||
| 540 | } | ||
| 541 | |||
| 542 | #[allow(dead_code, reason = "This is used in non test builds of Zed")] | ||
| 543 | async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { | ||
| 544 | let Some(connection_args) = binary.connection.as_ref() else { | ||
| 545 | return Err(anyhow!("No connection arguments provided")); | ||
| 546 | }; | ||
| 547 | |||
| 548 | let host = connection_args.host; | ||
| 549 | let port = connection_args.port; | ||
| 550 | |||
| 551 | let mut command = util::command::new_smol_command(&binary.command); | ||
| 552 | |||
| 553 | if let Some(cwd) = &binary.cwd { | ||
| 554 | command.current_dir(cwd); | ||
| 555 | } | ||
| 556 | |||
| 557 | if let Some(args) = &binary.arguments { | ||
| 558 | command.args(args); | ||
| 559 | } | ||
| 560 | |||
| 561 | if let Some(envs) = &binary.envs { | ||
| 562 | command.envs(envs); | ||
| 563 | } | ||
| 564 | |||
| 565 | command | ||
| 566 | .stdin(Stdio::null()) | ||
| 567 | .stdout(Stdio::piped()) | ||
| 568 | .stderr(Stdio::piped()) | ||
| 569 | .kill_on_drop(true); | ||
| 570 | |||
| 571 | let mut process = command | ||
| 572 | .spawn() | ||
| 573 | .with_context(|| "failed to start debug adapter.")?; | ||
| 574 | |||
| 575 | let address = SocketAddrV4::new(host, port); | ||
| 576 | |||
| 577 | let timeout = connection_args.timeout.unwrap_or_else(|| { | ||
| 578 | cx.update(|cx| DebuggerSettings::get_global(cx).timeout) | ||
| 579 | .unwrap_or(2000u64) | ||
| 580 | }); | ||
| 581 | |||
| 582 | let (rx, tx) = select! { | ||
| 583 | _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { | ||
| 584 | return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port))) | ||
| 585 | }, | ||
| 586 | result = cx.spawn(|cx| async move { | ||
| 587 | loop { | ||
| 588 | match TcpStream::connect(address).await { | ||
| 589 | Ok(stream) => return stream.split(), | ||
| 590 | Err(_) => { | ||
| 591 | cx.background_executor().timer(Duration::from_millis(100)).await; | ||
| 592 | } | ||
| 593 | } | ||
| 594 | } | ||
| 595 | }).fuse() => result | ||
| 596 | }; | ||
| 597 | log::info!( | ||
| 598 | "Debug adapter has connected to TCP server {}:{}", | ||
| 599 | host, | ||
| 600 | port | ||
| 601 | ); | ||
| 602 | let stdout = process.stdout.take(); | ||
| 603 | let stderr = process.stderr.take(); | ||
| 604 | |||
| 605 | let this = Self { | ||
| 606 | port, | ||
| 607 | host, | ||
| 608 | process: Mutex::new(process), | ||
| 609 | timeout, | ||
| 610 | }; | ||
| 611 | |||
| 612 | let pipe = TransportPipe::new( | ||
| 613 | Box::new(tx), | ||
| 614 | Box::new(BufReader::new(rx)), | ||
| 615 | stdout.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>), | ||
| 616 | stderr.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>), | ||
| 617 | ); | ||
| 618 | |||
| 619 | Ok((pipe, this)) | ||
| 620 | } | ||
| 621 | |||
| 622 | fn has_adapter_logs(&self) -> bool { | ||
| 623 | true | ||
| 624 | } | ||
| 625 | |||
| 626 | async fn kill(&self) -> Result<()> { | ||
| 627 | self.process.lock().await.kill()?; | ||
| 628 | |||
| 629 | Ok(()) | ||
| 630 | } | ||
| 631 | } | ||
| 632 | |||
| 633 | pub struct StdioTransport { | ||
| 634 | process: Mutex<Child>, | ||
| 635 | } | ||
| 636 | |||
| 637 | impl StdioTransport { | ||
| 638 | #[allow(dead_code, reason = "This is used in non test builds of Zed")] | ||
| 639 | async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> { | ||
| 640 | let mut command = util::command::new_smol_command(&binary.command); | ||
| 641 | |||
| 642 | if let Some(cwd) = &binary.cwd { | ||
| 643 | command.current_dir(cwd); | ||
| 644 | } | ||
| 645 | |||
| 646 | if let Some(args) = &binary.arguments { | ||
| 647 | command.args(args); | ||
| 648 | } | ||
| 649 | |||
| 650 | if let Some(envs) = &binary.envs { | ||
| 651 | command.envs(envs); | ||
| 652 | } | ||
| 653 | |||
| 654 | command | ||
| 655 | .stdin(Stdio::piped()) | ||
| 656 | .stdout(Stdio::piped()) | ||
| 657 | .stderr(Stdio::piped()) | ||
| 658 | .kill_on_drop(true); | ||
| 659 | |||
| 660 | let mut process = command | ||
| 661 | .spawn() | ||
| 662 | .with_context(|| "failed to spawn command.")?; | ||
| 663 | |||
| 664 | let stdin = process | ||
| 665 | .stdin | ||
| 666 | .take() | ||
| 667 | .ok_or_else(|| anyhow!("Failed to open stdin"))?; | ||
| 668 | let stdout = process | ||
| 669 | .stdout | ||
| 670 | .take() | ||
| 671 | .ok_or_else(|| anyhow!("Failed to open stdout"))?; | ||
| 672 | let stderr = process | ||
| 673 | .stderr | ||
| 674 | .take() | ||
| 675 | .map(|io_err| Box::new(io_err) as Box<dyn AsyncRead + Unpin + Send>); | ||
| 676 | |||
| 677 | if stderr.is_none() { | ||
| 678 | bail!( | ||
| 679 | "Failed to connect to stderr for debug adapter command {}", | ||
| 680 | &binary.command | ||
| 681 | ); | ||
| 682 | } | ||
| 683 | |||
| 684 | log::info!("Debug adapter has connected to stdio adapter"); | ||
| 685 | |||
| 686 | let process = Mutex::new(process); | ||
| 687 | |||
| 688 | Ok(( | ||
| 689 | TransportPipe::new( | ||
| 690 | Box::new(stdin), | ||
| 691 | Box::new(BufReader::new(stdout)), | ||
| 692 | None, | ||
| 693 | stderr, | ||
| 694 | ), | ||
| 695 | Self { process }, | ||
| 696 | )) | ||
| 697 | } | ||
| 698 | |||
| 699 | fn has_adapter_logs(&self) -> bool { | ||
| 700 | false | ||
| 701 | } | ||
| 702 | |||
| 703 | async fn kill(&self) -> Result<()> { | ||
| 704 | self.process.lock().await.kill()?; | ||
| 705 | Ok(()) | ||
| 706 | } | ||
| 707 | } | ||
| 708 | |||
| 709 | #[cfg(any(test, feature = "test-support"))] | ||
| 710 | type RequestHandler = Box< | ||
| 711 | dyn Send | ||
| 712 | + FnMut( | ||
| 713 | u64, | ||
| 714 | serde_json::Value, | ||
| 715 | Arc<Mutex<async_pipe::PipeWriter>>, | ||
| 716 | ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>, | ||
| 717 | >; | ||
| 718 | |||
| 719 | #[cfg(any(test, feature = "test-support"))] | ||
| 720 | type ResponseHandler = Box<dyn Send + Fn(Response)>; | ||
| 721 | |||
| 722 | #[cfg(any(test, feature = "test-support"))] | ||
| 723 | pub struct FakeTransport { | ||
| 724 | // for sending fake response back from adapter side | ||
| 725 | request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>, | ||
| 726 | // for reverse request responses | ||
| 727 | response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>, | ||
| 728 | } | ||
| 729 | |||
| 730 | #[cfg(any(test, feature = "test-support"))] | ||
| 731 | impl FakeTransport { | ||
| 732 | pub async fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F) | ||
| 733 | where | ||
| 734 | F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>, | ||
| 735 | { | ||
| 736 | self.request_handlers.lock().await.insert( | ||
| 737 | R::COMMAND, | ||
| 738 | Box::new( | ||
| 739 | move |seq, args, writer: Arc<Mutex<async_pipe::PipeWriter>>| { | ||
| 740 | let response = handler(seq, serde_json::from_value(args).unwrap()); | ||
| 741 | |||
| 742 | let message = serde_json::to_string(&Message::Response(Response { | ||
| 743 | seq: seq + 1, | ||
| 744 | request_seq: seq, | ||
| 745 | success: response.as_ref().is_ok(), | ||
| 746 | command: R::COMMAND.into(), | ||
| 747 | body: util::maybe!({ serde_json::to_value(response.ok()?).ok() }), | ||
| 748 | message: None, | ||
| 749 | })) | ||
| 750 | .unwrap(); | ||
| 751 | |||
| 752 | let writer = writer.clone(); | ||
| 753 | |||
| 754 | Box::pin(async move { | ||
| 755 | let mut writer = writer.lock().await; | ||
| 756 | writer | ||
| 757 | .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) | ||
| 758 | .await | ||
| 759 | .unwrap(); | ||
| 760 | writer.flush().await.unwrap(); | ||
| 761 | }) | ||
| 762 | }, | ||
| 763 | ), | ||
| 764 | ); | ||
| 765 | } | ||
| 766 | |||
| 767 | pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F) | ||
| 768 | where | ||
| 769 | F: 'static + Send + Fn(Response), | ||
| 770 | { | ||
| 771 | self.response_handlers | ||
| 772 | .lock() | ||
| 773 | .await | ||
| 774 | .insert(R::COMMAND, Box::new(handler)); | ||
| 775 | } | ||
| 776 | |||
| 777 | async fn start(cx: AsyncApp) -> Result<(TransportPipe, Self)> { | ||
| 778 | let this = Self { | ||
| 779 | request_handlers: Arc::new(Mutex::new(HashMap::default())), | ||
| 780 | response_handlers: Arc::new(Mutex::new(HashMap::default())), | ||
| 781 | }; | ||
| 782 | use dap_types::requests::{Request, RunInTerminal, StartDebugging}; | ||
| 783 | use serde_json::json; | ||
| 784 | |||
| 785 | let (stdin_writer, stdin_reader) = async_pipe::pipe(); | ||
| 786 | let (stdout_writer, stdout_reader) = async_pipe::pipe(); | ||
| 787 | |||
| 788 | let request_handlers = this.request_handlers.clone(); | ||
| 789 | let response_handlers = this.response_handlers.clone(); | ||
| 790 | let stdout_writer = Arc::new(Mutex::new(stdout_writer)); | ||
| 791 | |||
| 792 | cx.background_executor() | ||
| 793 | .spawn(async move { | ||
| 794 | let mut reader = BufReader::new(stdin_reader); | ||
| 795 | let mut buffer = String::new(); | ||
| 796 | |||
| 797 | loop { | ||
| 798 | let message = | ||
| 799 | TransportDelegate::receive_server_message(&mut reader, &mut buffer, None) | ||
| 800 | .await; | ||
| 801 | |||
| 802 | match message { | ||
| 803 | Err(error) => { | ||
| 804 | break anyhow!(error); | ||
| 805 | } | ||
| 806 | Ok(message) => { | ||
| 807 | match message { | ||
| 808 | Message::Request(request) => { | ||
| 809 | // redirect reverse requests to stdout writer/reader | ||
| 810 | if request.command == RunInTerminal::COMMAND | ||
| 811 | || request.command == StartDebugging::COMMAND | ||
| 812 | { | ||
| 813 | let message = | ||
| 814 | serde_json::to_string(&Message::Request(request)) | ||
| 815 | .unwrap(); | ||
| 816 | |||
| 817 | let mut writer = stdout_writer.lock().await; | ||
| 818 | writer | ||
| 819 | .write_all( | ||
| 820 | TransportDelegate::build_rpc_message(message) | ||
| 821 | .as_bytes(), | ||
| 822 | ) | ||
| 823 | .await | ||
| 824 | .unwrap(); | ||
| 825 | writer.flush().await.unwrap(); | ||
| 826 | } else { | ||
| 827 | if let Some(handle) = request_handlers | ||
| 828 | .lock() | ||
| 829 | .await | ||
| 830 | .get_mut(request.command.as_str()) | ||
| 831 | { | ||
| 832 | handle( | ||
| 833 | request.seq, | ||
| 834 | request.arguments.unwrap_or(json!({})), | ||
| 835 | stdout_writer.clone(), | ||
| 836 | ) | ||
| 837 | .await; | ||
| 838 | } else { | ||
| 839 | log::error!( | ||
| 840 | "No request handler for {}", | ||
| 841 | request.command | ||
| 842 | ); | ||
| 843 | } | ||
| 844 | } | ||
| 845 | } | ||
| 846 | Message::Event(event) => { | ||
| 847 | let message = | ||
| 848 | serde_json::to_string(&Message::Event(event)).unwrap(); | ||
| 849 | |||
| 850 | let mut writer = stdout_writer.lock().await; | ||
| 851 | writer | ||
| 852 | .write_all( | ||
| 853 | TransportDelegate::build_rpc_message(message) | ||
| 854 | .as_bytes(), | ||
| 855 | ) | ||
| 856 | .await | ||
| 857 | .unwrap(); | ||
| 858 | writer.flush().await.unwrap(); | ||
| 859 | } | ||
| 860 | Message::Response(response) => { | ||
| 861 | if let Some(handle) = response_handlers | ||
| 862 | .lock() | ||
| 863 | .await | ||
| 864 | .get(response.command.as_str()) | ||
| 865 | { | ||
| 866 | handle(response); | ||
| 867 | } else { | ||
| 868 | log::error!("No response handler for {}", response.command); | ||
| 869 | } | ||
| 870 | } | ||
| 871 | } | ||
| 872 | } | ||
| 873 | } | ||
| 874 | } | ||
| 875 | }) | ||
| 876 | .detach(); | ||
| 877 | |||
| 878 | Ok(( | ||
| 879 | TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None), | ||
| 880 | this, | ||
| 881 | )) | ||
| 882 | } | ||
| 883 | |||
| 884 | fn has_adapter_logs(&self) -> bool { | ||
| 885 | false | ||
| 886 | } | ||
| 887 | |||
| 888 | async fn kill(&self) -> Result<()> { | ||
| 889 | Ok(()) | ||
| 890 | } | ||
| 891 | } | ||
| @@ -0,0 +1,41 @@ | |||
| 1 | [package] | ||
| 2 | name = "dap_adapters" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition.workspace = true | ||
| 5 | publish.workspace = true | ||
| 6 | license = "GPL-3.0-or-later" | ||
| 7 | |||
| 8 | [features] | ||
| 9 | test-support = [ | ||
| 10 | "dap/test-support", | ||
| 11 | "gpui/test-support", | ||
| 12 | "task/test-support", | ||
| 13 | "util/test-support", | ||
| 14 | ] | ||
| 15 | |||
| 16 | [lints] | ||
| 17 | workspace = true | ||
| 18 | |||
| 19 | [lib] | ||
| 20 | path = "src/dap_adapters.rs" | ||
| 21 | doctest = false | ||
| 22 | |||
| 23 | [dependencies] | ||
| 24 | anyhow.workspace = true | ||
| 25 | async-trait.workspace = true | ||
| 26 | dap.workspace = true | ||
| 27 | gpui.workspace = true | ||
| 28 | language.workspace = true | ||
| 29 | paths.workspace = true | ||
| 30 | regex.workspace = true | ||
| 31 | serde.workspace = true | ||
| 32 | serde_json.workspace = true | ||
| 33 | sysinfo.workspace = true | ||
| 34 | task.workspace = true | ||
| 35 | util.workspace = true | ||
| 36 | |||
| 37 | [dev-dependencies] | ||
| 38 | dap = { workspace = true, features = ["test-support"] } | ||
| 39 | gpui = { workspace = true, features = ["test-support"] } | ||
| 40 | task = { workspace = true, features = ["test-support"] } | ||
| 41 | util = { workspace = true, features = ["test-support"] } | ||
| @@ -0,0 +1 @@ | |||
| 1 | ../../LICENSE-GPL | ||
| @@ -0,0 +1,84 @@ | |||
| 1 | use dap::transport::TcpTransport; | ||
| 2 | use gpui::AsyncApp; | ||
| 3 | use serde_json::Value; | ||
| 4 | use std::{collections::HashMap, ffi::OsString, path::PathBuf}; | ||
| 5 | use sysinfo::{Pid, Process}; | ||
| 6 | use task::DebugAdapterConfig; | ||
| 7 | |||
| 8 | use crate::*; | ||
| 9 | |||
| 10 | pub(crate) struct CustomDebugAdapter { | ||
| 11 | custom_args: CustomArgs, | ||
| 12 | } | ||
| 13 | |||
| 14 | impl CustomDebugAdapter { | ||
| 15 | const ADAPTER_NAME: &'static str = "custom_dap"; | ||
| 16 | |||
| 17 | pub(crate) async fn new(custom_args: CustomArgs) -> Result<Self> { | ||
| 18 | Ok(CustomDebugAdapter { custom_args }) | ||
| 19 | } | ||
| 20 | |||
| 21 | pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> { | ||
| 22 | processes.iter().collect::<Vec<_>>() | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | #[async_trait(?Send)] | ||
| 27 | impl DebugAdapter for CustomDebugAdapter { | ||
| 28 | fn name(&self) -> DebugAdapterName { | ||
| 29 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 30 | } | ||
| 31 | |||
| 32 | async fn get_binary( | ||
| 33 | &self, | ||
| 34 | _: &dyn DapDelegate, | ||
| 35 | config: &DebugAdapterConfig, | ||
| 36 | _: Option<PathBuf>, | ||
| 37 | _: &mut AsyncApp, | ||
| 38 | ) -> Result<DebugAdapterBinary> { | ||
| 39 | let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection | ||
| 40 | { | ||
| 41 | Some(adapters::TcpArguments { | ||
| 42 | host: connection.host(), | ||
| 43 | port: TcpTransport::port(&connection).await?, | ||
| 44 | timeout: connection.timeout, | ||
| 45 | }) | ||
| 46 | } else { | ||
| 47 | None | ||
| 48 | }; | ||
| 49 | let ret = DebugAdapterBinary { | ||
| 50 | command: self.custom_args.command.clone(), | ||
| 51 | arguments: self | ||
| 52 | .custom_args | ||
| 53 | .args | ||
| 54 | .clone() | ||
| 55 | .map(|args| args.iter().map(OsString::from).collect()), | ||
| 56 | cwd: config.cwd.clone(), | ||
| 57 | envs: self.custom_args.envs.clone(), | ||
| 58 | connection, | ||
| 59 | }; | ||
| 60 | Ok(ret) | ||
| 61 | } | ||
| 62 | |||
| 63 | async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> { | ||
| 64 | bail!("Custom debug adapters don't have latest versions") | ||
| 65 | } | ||
| 66 | |||
| 67 | async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> { | ||
| 68 | bail!("Custom debug adapters cannot be installed") | ||
| 69 | } | ||
| 70 | |||
| 71 | async fn get_installed_binary( | ||
| 72 | &self, | ||
| 73 | _: &dyn DapDelegate, | ||
| 74 | _: &DebugAdapterConfig, | ||
| 75 | _: Option<PathBuf>, | ||
| 76 | _: &mut AsyncApp, | ||
| 77 | ) -> Result<DebugAdapterBinary> { | ||
| 78 | bail!("Custom debug adapters cannot be installed") | ||
| 79 | } | ||
| 80 | |||
| 81 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 82 | json!({"program": config.program}) | ||
| 83 | } | ||
| 84 | } | ||
| @@ -0,0 +1,67 @@ | |||
| 1 | mod custom; | ||
| 2 | #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] | ||
| 3 | mod gdb; | ||
| 4 | mod go; | ||
| 5 | mod javascript; | ||
| 6 | mod lldb; | ||
| 7 | mod php; | ||
| 8 | mod python; | ||
| 9 | |||
| 10 | use std::{collections::HashMap, sync::Arc}; | ||
| 11 | |||
| 12 | use anyhow::{anyhow, bail, Result}; | ||
| 13 | use async_trait::async_trait; | ||
| 14 | use custom::CustomDebugAdapter; | ||
| 15 | use dap::adapters::{ | ||
| 16 | self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, | ||
| 17 | GithubRepo, | ||
| 18 | }; | ||
| 19 | #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] | ||
| 20 | use gdb::GdbDebugAdapter; | ||
| 21 | use go::GoDebugAdapter; | ||
| 22 | use javascript::JsDebugAdapter; | ||
| 23 | use lldb::LldbDebugAdapter; | ||
| 24 | use php::PhpDebugAdapter; | ||
| 25 | use python::PythonDebugAdapter; | ||
| 26 | use serde_json::{json, Value}; | ||
| 27 | use sysinfo::{Pid, Process}; | ||
| 28 | use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; | ||
| 29 | |||
| 30 | pub async fn build_adapter(kind: &DebugAdapterKind) -> Result<Arc<dyn DebugAdapter>> { | ||
| 31 | match kind { | ||
| 32 | DebugAdapterKind::Custom(start_args) => { | ||
| 33 | Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?)) | ||
| 34 | } | ||
| 35 | DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)), | ||
| 36 | DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)), | ||
| 37 | DebugAdapterKind::Javascript(host) => { | ||
| 38 | Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?)) | ||
| 39 | } | ||
| 40 | DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())), | ||
| 41 | DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)), | ||
| 42 | #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] | ||
| 43 | DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())), | ||
| 44 | #[cfg(any(test, feature = "test-support"))] | ||
| 45 | DebugAdapterKind::Fake(_) => Ok(Arc::new(dap::adapters::FakeAdapter::new())), | ||
| 46 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 47 | #[allow(unreachable_patterns)] | ||
| 48 | _ => unreachable!("Fake variant only exists with test-support feature"), | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | pub fn attach_processes<'a>( | ||
| 53 | kind: &DebugAdapterKind, | ||
| 54 | processes: &'a HashMap<Pid, Process>, | ||
| 55 | ) -> Vec<(&'a Pid, &'a Process)> { | ||
| 56 | match kind { | ||
| 57 | #[cfg(any(test, feature = "test-support"))] | ||
| 58 | DebugAdapterKind::Fake(_) => processes | ||
| 59 | .iter() | ||
| 60 | .filter(|(pid, _)| pid.as_u32() == std::process::id()) | ||
| 61 | .collect::<Vec<_>>(), | ||
| 62 | DebugAdapterKind::Custom(_) => CustomDebugAdapter::attach_processes(processes), | ||
| 63 | DebugAdapterKind::Javascript(_) => JsDebugAdapter::attach_processes(processes), | ||
| 64 | DebugAdapterKind::Lldb => LldbDebugAdapter::attach_processes(processes), | ||
| 65 | _ => processes.iter().collect::<Vec<_>>(), | ||
| 66 | } | ||
| 67 | } | ||
| @@ -0,0 +1,83 @@ | |||
| 1 | use std::ffi::OsStr; | ||
| 2 | |||
| 3 | use anyhow::Result; | ||
| 4 | use async_trait::async_trait; | ||
| 5 | use gpui::AsyncApp; | ||
| 6 | use task::DebugAdapterConfig; | ||
| 7 | |||
| 8 | use crate::*; | ||
| 9 | |||
| 10 | pub(crate) struct GdbDebugAdapter {} | ||
| 11 | |||
| 12 | impl GdbDebugAdapter { | ||
| 13 | const ADAPTER_NAME: &'static str = "gdb"; | ||
| 14 | |||
| 15 | pub(crate) fn new() -> Self { | ||
| 16 | GdbDebugAdapter {} | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | #[async_trait(?Send)] | ||
| 21 | impl DebugAdapter for GdbDebugAdapter { | ||
| 22 | fn name(&self) -> DebugAdapterName { | ||
| 23 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 24 | } | ||
| 25 | |||
| 26 | async fn get_binary( | ||
| 27 | &self, | ||
| 28 | delegate: &dyn DapDelegate, | ||
| 29 | config: &DebugAdapterConfig, | ||
| 30 | user_installed_path: Option<std::path::PathBuf>, | ||
| 31 | _: &mut AsyncApp, | ||
| 32 | ) -> Result<DebugAdapterBinary> { | ||
| 33 | let user_setting_path = user_installed_path | ||
| 34 | .filter(|p| p.exists()) | ||
| 35 | .and_then(|p| p.to_str().map(|s| s.to_string())); | ||
| 36 | |||
| 37 | /* GDB implements DAP natively so just need to */ | ||
| 38 | let gdb_path = delegate | ||
| 39 | .which(OsStr::new("gdb")) | ||
| 40 | .and_then(|p| p.to_str().map(|s| s.to_string())) | ||
| 41 | .ok_or(anyhow!("Could not find gdb in path")); | ||
| 42 | |||
| 43 | if gdb_path.is_err() && user_setting_path.is_none() { | ||
| 44 | bail!("Could not find gdb path or it's not installed"); | ||
| 45 | } | ||
| 46 | |||
| 47 | let gdb_path = user_setting_path.unwrap_or(gdb_path?); | ||
| 48 | |||
| 49 | Ok(DebugAdapterBinary { | ||
| 50 | command: gdb_path, | ||
| 51 | arguments: Some(vec!["-i=dap".into()]), | ||
| 52 | envs: None, | ||
| 53 | cwd: config.cwd.clone(), | ||
| 54 | connection: None, | ||
| 55 | }) | ||
| 56 | } | ||
| 57 | |||
| 58 | async fn install_binary( | ||
| 59 | &self, | ||
| 60 | _version: AdapterVersion, | ||
| 61 | _delegate: &dyn DapDelegate, | ||
| 62 | ) -> Result<()> { | ||
| 63 | unimplemented!("GDB debug adapter cannot be installed by Zed (yet)") | ||
| 64 | } | ||
| 65 | |||
| 66 | async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> { | ||
| 67 | unimplemented!("Fetch latest GDB version not implemented (yet)") | ||
| 68 | } | ||
| 69 | |||
| 70 | async fn get_installed_binary( | ||
| 71 | &self, | ||
| 72 | _: &dyn DapDelegate, | ||
| 73 | _: &DebugAdapterConfig, | ||
| 74 | _: Option<std::path::PathBuf>, | ||
| 75 | _: &mut AsyncApp, | ||
| 76 | ) -> Result<DebugAdapterBinary> { | ||
| 77 | unimplemented!("GDB cannot be installed by Zed (yet)") | ||
| 78 | } | ||
| 79 | |||
| 80 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 81 | json!({"program": config.program, "cwd": config.cwd}) | ||
| 82 | } | ||
| 83 | } | ||
| @@ -0,0 +1,100 @@ | |||
| 1 | use dap::transport::TcpTransport; | ||
| 2 | use gpui::AsyncApp; | ||
| 3 | use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; | ||
| 4 | |||
| 5 | use crate::*; | ||
| 6 | |||
| 7 | pub(crate) struct GoDebugAdapter { | ||
| 8 | port: u16, | ||
| 9 | host: Ipv4Addr, | ||
| 10 | timeout: Option<u64>, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl GoDebugAdapter { | ||
| 14 | const ADAPTER_NAME: &'static str = "delve"; | ||
| 15 | |||
| 16 | pub(crate) async fn new(host: &TCPHost) -> Result<Self> { | ||
| 17 | Ok(GoDebugAdapter { | ||
| 18 | port: TcpTransport::port(host).await?, | ||
| 19 | host: host.host(), | ||
| 20 | timeout: host.timeout, | ||
| 21 | }) | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | #[async_trait(?Send)] | ||
| 26 | impl DebugAdapter for GoDebugAdapter { | ||
| 27 | fn name(&self) -> DebugAdapterName { | ||
| 28 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 29 | } | ||
| 30 | |||
| 31 | async fn get_binary( | ||
| 32 | &self, | ||
| 33 | delegate: &dyn DapDelegate, | ||
| 34 | config: &DebugAdapterConfig, | ||
| 35 | user_installed_path: Option<PathBuf>, | ||
| 36 | cx: &mut AsyncApp, | ||
| 37 | ) -> Result<DebugAdapterBinary> { | ||
| 38 | self.get_installed_binary(delegate, config, user_installed_path, cx) | ||
| 39 | .await | ||
| 40 | } | ||
| 41 | |||
| 42 | async fn fetch_latest_adapter_version( | ||
| 43 | &self, | ||
| 44 | _delegate: &dyn DapDelegate, | ||
| 45 | ) -> Result<AdapterVersion> { | ||
| 46 | unimplemented!("This adapter is used from path for now"); | ||
| 47 | } | ||
| 48 | |||
| 49 | async fn install_binary( | ||
| 50 | &self, | ||
| 51 | version: AdapterVersion, | ||
| 52 | delegate: &dyn DapDelegate, | ||
| 53 | ) -> Result<()> { | ||
| 54 | adapters::download_adapter_from_github( | ||
| 55 | self.name(), | ||
| 56 | version, | ||
| 57 | adapters::DownloadedFileType::Zip, | ||
| 58 | delegate, | ||
| 59 | ) | ||
| 60 | .await?; | ||
| 61 | Ok(()) | ||
| 62 | } | ||
| 63 | |||
| 64 | async fn get_installed_binary( | ||
| 65 | &self, | ||
| 66 | delegate: &dyn DapDelegate, | ||
| 67 | config: &DebugAdapterConfig, | ||
| 68 | _: Option<PathBuf>, | ||
| 69 | _: &mut AsyncApp, | ||
| 70 | ) -> Result<DebugAdapterBinary> { | ||
| 71 | let delve_path = delegate | ||
| 72 | .which(OsStr::new("dlv")) | ||
| 73 | .and_then(|p| p.to_str().map(|p| p.to_string())) | ||
| 74 | .ok_or(anyhow!("Dlv not found in path"))?; | ||
| 75 | |||
| 76 | Ok(DebugAdapterBinary { | ||
| 77 | command: delve_path, | ||
| 78 | arguments: Some(vec![ | ||
| 79 | "dap".into(), | ||
| 80 | "--listen".into(), | ||
| 81 | format!("{}:{}", self.host, self.port).into(), | ||
| 82 | ]), | ||
| 83 | cwd: config.cwd.clone(), | ||
| 84 | envs: None, | ||
| 85 | connection: Some(adapters::TcpArguments { | ||
| 86 | host: self.host, | ||
| 87 | port: self.port, | ||
| 88 | timeout: self.timeout, | ||
| 89 | }), | ||
| 90 | }) | ||
| 91 | } | ||
| 92 | |||
| 93 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 94 | json!({ | ||
| 95 | "program": config.program, | ||
| 96 | "cwd": config.cwd, | ||
| 97 | "subProcess": true, | ||
| 98 | }) | ||
| 99 | } | ||
| 100 | } | ||
| @@ -0,0 +1,148 @@ | |||
| 1 | use adapters::latest_github_release; | ||
| 2 | use dap::transport::TcpTransport; | ||
| 3 | use gpui::AsyncApp; | ||
| 4 | use regex::Regex; | ||
| 5 | use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf}; | ||
| 6 | use sysinfo::{Pid, Process}; | ||
| 7 | use task::DebugRequestType; | ||
| 8 | |||
| 9 | use crate::*; | ||
| 10 | |||
| 11 | pub(crate) struct JsDebugAdapter { | ||
| 12 | port: u16, | ||
| 13 | host: Ipv4Addr, | ||
| 14 | timeout: Option<u64>, | ||
| 15 | } | ||
| 16 | |||
| 17 | impl JsDebugAdapter { | ||
| 18 | const ADAPTER_NAME: &'static str = "vscode-js-debug"; | ||
| 19 | const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js"; | ||
| 20 | |||
| 21 | pub(crate) async fn new(host: TCPHost) -> Result<Self> { | ||
| 22 | Ok(JsDebugAdapter { | ||
| 23 | host: host.host(), | ||
| 24 | timeout: host.timeout, | ||
| 25 | port: TcpTransport::port(&host).await?, | ||
| 26 | }) | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> { | ||
| 30 | let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap(); | ||
| 31 | |||
| 32 | processes | ||
| 33 | .iter() | ||
| 34 | .filter(|(_, process)| regex.is_match(&process.name().to_string_lossy())) | ||
| 35 | .collect::<Vec<_>>() | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | #[async_trait(?Send)] | ||
| 40 | impl DebugAdapter for JsDebugAdapter { | ||
| 41 | fn name(&self) -> DebugAdapterName { | ||
| 42 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 43 | } | ||
| 44 | |||
| 45 | async fn fetch_latest_adapter_version( | ||
| 46 | &self, | ||
| 47 | delegate: &dyn DapDelegate, | ||
| 48 | ) -> Result<AdapterVersion> { | ||
| 49 | let release = latest_github_release( | ||
| 50 | &format!("{}/{}", "microsoft", Self::ADAPTER_NAME), | ||
| 51 | true, | ||
| 52 | false, | ||
| 53 | delegate.http_client(), | ||
| 54 | ) | ||
| 55 | .await?; | ||
| 56 | |||
| 57 | let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name); | ||
| 58 | |||
| 59 | Ok(AdapterVersion { | ||
| 60 | tag_name: release.tag_name, | ||
| 61 | url: release | ||
| 62 | .assets | ||
| 63 | .iter() | ||
| 64 | .find(|asset| asset.name == asset_name) | ||
| 65 | .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? | ||
| 66 | .browser_download_url | ||
| 67 | .clone(), | ||
| 68 | }) | ||
| 69 | } | ||
| 70 | |||
| 71 | async fn get_installed_binary( | ||
| 72 | &self, | ||
| 73 | delegate: &dyn DapDelegate, | ||
| 74 | config: &DebugAdapterConfig, | ||
| 75 | user_installed_path: Option<PathBuf>, | ||
| 76 | _: &mut AsyncApp, | ||
| 77 | ) -> Result<DebugAdapterBinary> { | ||
| 78 | let adapter_path = if let Some(user_installed_path) = user_installed_path { | ||
| 79 | user_installed_path | ||
| 80 | } else { | ||
| 81 | let adapter_path = paths::debug_adapters_dir().join(self.name()); | ||
| 82 | |||
| 83 | let file_name_prefix = format!("{}_", self.name()); | ||
| 84 | |||
| 85 | util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { | ||
| 86 | file_name.starts_with(&file_name_prefix) | ||
| 87 | }) | ||
| 88 | .await | ||
| 89 | .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))? | ||
| 90 | }; | ||
| 91 | |||
| 92 | Ok(DebugAdapterBinary { | ||
| 93 | command: delegate | ||
| 94 | .node_runtime() | ||
| 95 | .binary_path() | ||
| 96 | .await? | ||
| 97 | .to_string_lossy() | ||
| 98 | .into_owned(), | ||
| 99 | arguments: Some(vec![ | ||
| 100 | adapter_path.join(Self::ADAPTER_PATH).into(), | ||
| 101 | self.port.to_string().into(), | ||
| 102 | self.host.to_string().into(), | ||
| 103 | ]), | ||
| 104 | cwd: config.cwd.clone(), | ||
| 105 | envs: None, | ||
| 106 | connection: Some(adapters::TcpArguments { | ||
| 107 | host: self.host, | ||
| 108 | port: self.port, | ||
| 109 | timeout: self.timeout, | ||
| 110 | }), | ||
| 111 | }) | ||
| 112 | } | ||
| 113 | |||
| 114 | async fn install_binary( | ||
| 115 | &self, | ||
| 116 | version: AdapterVersion, | ||
| 117 | delegate: &dyn DapDelegate, | ||
| 118 | ) -> Result<()> { | ||
| 119 | adapters::download_adapter_from_github( | ||
| 120 | self.name(), | ||
| 121 | version, | ||
| 122 | adapters::DownloadedFileType::GzipTar, | ||
| 123 | delegate, | ||
| 124 | ) | ||
| 125 | .await?; | ||
| 126 | |||
| 127 | return Ok(()); | ||
| 128 | } | ||
| 129 | |||
| 130 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 131 | let pid = if let DebugRequestType::Attach(attach_config) = &config.request { | ||
| 132 | attach_config.process_id | ||
| 133 | } else { | ||
| 134 | None | ||
| 135 | }; | ||
| 136 | |||
| 137 | json!({ | ||
| 138 | "program": config.program, | ||
| 139 | "type": "pwa-node", | ||
| 140 | "request": match config.request { | ||
| 141 | DebugRequestType::Launch => "launch", | ||
| 142 | DebugRequestType::Attach(_) => "attach", | ||
| 143 | }, | ||
| 144 | "processId": pid, | ||
| 145 | "cwd": config.cwd, | ||
| 146 | }) | ||
| 147 | } | ||
| 148 | } | ||
| @@ -0,0 +1,104 @@ | |||
| 1 | use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; | ||
| 2 | |||
| 3 | use anyhow::Result; | ||
| 4 | use async_trait::async_trait; | ||
| 5 | use gpui::AsyncApp; | ||
| 6 | use sysinfo::{Pid, Process}; | ||
| 7 | use task::{DebugAdapterConfig, DebugRequestType}; | ||
| 8 | |||
| 9 | use crate::*; | ||
| 10 | |||
| 11 | pub(crate) struct LldbDebugAdapter {} | ||
| 12 | |||
| 13 | impl LldbDebugAdapter { | ||
| 14 | const ADAPTER_NAME: &'static str = "lldb"; | ||
| 15 | |||
| 16 | pub(crate) fn new() -> Self { | ||
| 17 | LldbDebugAdapter {} | ||
| 18 | } | ||
| 19 | |||
| 20 | pub fn attach_processes(processes: &HashMap<Pid, Process>) -> Vec<(&Pid, &Process)> { | ||
| 21 | processes.iter().collect::<Vec<_>>() | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | #[async_trait(?Send)] | ||
| 26 | impl DebugAdapter for LldbDebugAdapter { | ||
| 27 | fn name(&self) -> DebugAdapterName { | ||
| 28 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 29 | } | ||
| 30 | |||
| 31 | async fn get_binary( | ||
| 32 | &self, | ||
| 33 | delegate: &dyn DapDelegate, | ||
| 34 | config: &DebugAdapterConfig, | ||
| 35 | user_installed_path: Option<PathBuf>, | ||
| 36 | _: &mut AsyncApp, | ||
| 37 | ) -> Result<DebugAdapterBinary> { | ||
| 38 | let lldb_dap_path = if let Some(user_installed_path) = user_installed_path { | ||
| 39 | user_installed_path.to_string_lossy().into() | ||
| 40 | } else if cfg!(target_os = "macos") { | ||
| 41 | util::command::new_smol_command("xcrun") | ||
| 42 | .args(&["-f", "lldb-dap"]) | ||
| 43 | .output() | ||
| 44 | .await | ||
| 45 | .ok() | ||
| 46 | .and_then(|output| String::from_utf8(output.stdout).ok()) | ||
| 47 | .map(|path| path.trim().to_string()) | ||
| 48 | .ok_or(anyhow!("Failed to find lldb-dap in user's path"))? | ||
| 49 | } else { | ||
| 50 | delegate | ||
| 51 | .which(OsStr::new("lldb-dap")) | ||
| 52 | .and_then(|p| p.to_str().map(|s| s.to_string())) | ||
| 53 | .ok_or(anyhow!("Could not find lldb-dap in path"))? | ||
| 54 | }; | ||
| 55 | |||
| 56 | Ok(DebugAdapterBinary { | ||
| 57 | command: lldb_dap_path, | ||
| 58 | arguments: None, | ||
| 59 | envs: None, | ||
| 60 | cwd: config.cwd.clone(), | ||
| 61 | connection: None, | ||
| 62 | }) | ||
| 63 | } | ||
| 64 | |||
| 65 | async fn install_binary( | ||
| 66 | &self, | ||
| 67 | _version: AdapterVersion, | ||
| 68 | _delegate: &dyn DapDelegate, | ||
| 69 | ) -> Result<()> { | ||
| 70 | unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)") | ||
| 71 | } | ||
| 72 | |||
| 73 | async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> { | ||
| 74 | unimplemented!("Fetch latest adapter version not implemented for lldb (yet)") | ||
| 75 | } | ||
| 76 | |||
| 77 | async fn get_installed_binary( | ||
| 78 | &self, | ||
| 79 | _: &dyn DapDelegate, | ||
| 80 | _: &DebugAdapterConfig, | ||
| 81 | _: Option<PathBuf>, | ||
| 82 | _: &mut AsyncApp, | ||
| 83 | ) -> Result<DebugAdapterBinary> { | ||
| 84 | unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)") | ||
| 85 | } | ||
| 86 | |||
| 87 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 88 | let pid = if let DebugRequestType::Attach(attach_config) = &config.request { | ||
| 89 | attach_config.process_id | ||
| 90 | } else { | ||
| 91 | None | ||
| 92 | }; | ||
| 93 | |||
| 94 | json!({ | ||
| 95 | "program": config.program, | ||
| 96 | "request": match config.request { | ||
| 97 | DebugRequestType::Launch => "launch", | ||
| 98 | DebugRequestType::Attach(_) => "attach", | ||
| 99 | }, | ||
| 100 | "pid": pid, | ||
| 101 | "cwd": config.cwd, | ||
| 102 | }) | ||
| 103 | } | ||
| 104 | } | ||
| @@ -0,0 +1,123 @@ | |||
| 1 | use adapters::latest_github_release; | ||
| 2 | use dap::{adapters::TcpArguments, transport::TcpTransport}; | ||
| 3 | use gpui::AsyncApp; | ||
| 4 | use std::{net::Ipv4Addr, path::PathBuf}; | ||
| 5 | |||
| 6 | use crate::*; | ||
| 7 | |||
| 8 | pub(crate) struct PhpDebugAdapter { | ||
| 9 | port: u16, | ||
| 10 | host: Ipv4Addr, | ||
| 11 | timeout: Option<u64>, | ||
| 12 | } | ||
| 13 | |||
| 14 | impl PhpDebugAdapter { | ||
| 15 | const ADAPTER_NAME: &'static str = "vscode-php-debug"; | ||
| 16 | const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; | ||
| 17 | |||
| 18 | pub(crate) async fn new(host: TCPHost) -> Result<Self> { | ||
| 19 | Ok(PhpDebugAdapter { | ||
| 20 | port: TcpTransport::port(&host).await?, | ||
| 21 | host: host.host(), | ||
| 22 | timeout: host.timeout, | ||
| 23 | }) | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | #[async_trait(?Send)] | ||
| 28 | impl DebugAdapter for PhpDebugAdapter { | ||
| 29 | fn name(&self) -> DebugAdapterName { | ||
| 30 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 31 | } | ||
| 32 | |||
| 33 | async fn fetch_latest_adapter_version( | ||
| 34 | &self, | ||
| 35 | delegate: &dyn DapDelegate, | ||
| 36 | ) -> Result<AdapterVersion> { | ||
| 37 | let release = latest_github_release( | ||
| 38 | &format!("{}/{}", "xdebug", Self::ADAPTER_NAME), | ||
| 39 | true, | ||
| 40 | false, | ||
| 41 | delegate.http_client(), | ||
| 42 | ) | ||
| 43 | .await?; | ||
| 44 | |||
| 45 | let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", "")); | ||
| 46 | |||
| 47 | Ok(AdapterVersion { | ||
| 48 | tag_name: release.tag_name, | ||
| 49 | url: release | ||
| 50 | .assets | ||
| 51 | .iter() | ||
| 52 | .find(|asset| asset.name == asset_name) | ||
| 53 | .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? | ||
| 54 | .browser_download_url | ||
| 55 | .clone(), | ||
| 56 | }) | ||
| 57 | } | ||
| 58 | |||
| 59 | async fn get_installed_binary( | ||
| 60 | &self, | ||
| 61 | delegate: &dyn DapDelegate, | ||
| 62 | config: &DebugAdapterConfig, | ||
| 63 | user_installed_path: Option<PathBuf>, | ||
| 64 | _: &mut AsyncApp, | ||
| 65 | ) -> Result<DebugAdapterBinary> { | ||
| 66 | let adapter_path = if let Some(user_installed_path) = user_installed_path { | ||
| 67 | user_installed_path | ||
| 68 | } else { | ||
| 69 | let adapter_path = paths::debug_adapters_dir().join(self.name()); | ||
| 70 | |||
| 71 | let file_name_prefix = format!("{}_", self.name()); | ||
| 72 | |||
| 73 | util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { | ||
| 74 | file_name.starts_with(&file_name_prefix) | ||
| 75 | }) | ||
| 76 | .await | ||
| 77 | .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))? | ||
| 78 | }; | ||
| 79 | |||
| 80 | Ok(DebugAdapterBinary { | ||
| 81 | command: delegate | ||
| 82 | .node_runtime() | ||
| 83 | .binary_path() | ||
| 84 | .await? | ||
| 85 | .to_string_lossy() | ||
| 86 | .into_owned(), | ||
| 87 | arguments: Some(vec![ | ||
| 88 | adapter_path.join(Self::ADAPTER_PATH).into(), | ||
| 89 | format!("--server={}", self.port).into(), | ||
| 90 | ]), | ||
| 91 | connection: Some(TcpArguments { | ||
| 92 | port: self.port, | ||
| 93 | host: self.host, | ||
| 94 | timeout: self.timeout, | ||
| 95 | }), | ||
| 96 | cwd: config.cwd.clone(), | ||
| 97 | envs: None, | ||
| 98 | }) | ||
| 99 | } | ||
| 100 | |||
| 101 | async fn install_binary( | ||
| 102 | &self, | ||
| 103 | version: AdapterVersion, | ||
| 104 | delegate: &dyn DapDelegate, | ||
| 105 | ) -> Result<()> { | ||
| 106 | adapters::download_adapter_from_github( | ||
| 107 | self.name(), | ||
| 108 | version, | ||
| 109 | adapters::DownloadedFileType::Vsix, | ||
| 110 | delegate, | ||
| 111 | ) | ||
| 112 | .await?; | ||
| 113 | |||
| 114 | Ok(()) | ||
| 115 | } | ||
| 116 | |||
| 117 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 118 | json!({ | ||
| 119 | "program": config.program, | ||
| 120 | "cwd": config.cwd, | ||
| 121 | }) | ||
| 122 | } | ||
| 123 | } | ||
| @@ -0,0 +1,142 @@ | |||
| 1 | use crate::*; | ||
| 2 | use dap::transport::TcpTransport; | ||
| 3 | use gpui::AsyncApp; | ||
| 4 | use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; | ||
| 5 | |||
| 6 | pub(crate) struct PythonDebugAdapter { | ||
| 7 | port: u16, | ||
| 8 | host: Ipv4Addr, | ||
| 9 | timeout: Option<u64>, | ||
| 10 | } | ||
| 11 | |||
| 12 | impl PythonDebugAdapter { | ||
| 13 | const ADAPTER_NAME: &'static str = "debugpy"; | ||
| 14 | const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; | ||
| 15 | const LANGUAGE_NAME: &'static str = "Python"; | ||
| 16 | |||
| 17 | pub(crate) async fn new(host: &TCPHost) -> Result<Self> { | ||
| 18 | Ok(PythonDebugAdapter { | ||
| 19 | port: TcpTransport::port(host).await?, | ||
| 20 | host: host.host(), | ||
| 21 | timeout: host.timeout, | ||
| 22 | }) | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | #[async_trait(?Send)] | ||
| 27 | impl DebugAdapter for PythonDebugAdapter { | ||
| 28 | fn name(&self) -> DebugAdapterName { | ||
| 29 | DebugAdapterName(Self::ADAPTER_NAME.into()) | ||
| 30 | } | ||
| 31 | |||
| 32 | async fn fetch_latest_adapter_version( | ||
| 33 | &self, | ||
| 34 | delegate: &dyn DapDelegate, | ||
| 35 | ) -> Result<AdapterVersion> { | ||
| 36 | let github_repo = GithubRepo { | ||
| 37 | repo_name: Self::ADAPTER_NAME.into(), | ||
| 38 | repo_owner: "microsoft".into(), | ||
| 39 | }; | ||
| 40 | |||
| 41 | adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await | ||
| 42 | } | ||
| 43 | |||
| 44 | async fn install_binary( | ||
| 45 | &self, | ||
| 46 | version: AdapterVersion, | ||
| 47 | delegate: &dyn DapDelegate, | ||
| 48 | ) -> Result<()> { | ||
| 49 | let version_path = adapters::download_adapter_from_github( | ||
| 50 | self.name(), | ||
| 51 | version, | ||
| 52 | adapters::DownloadedFileType::Zip, | ||
| 53 | delegate, | ||
| 54 | ) | ||
| 55 | .await?; | ||
| 56 | |||
| 57 | // only needed when you install the latest version for the first time | ||
| 58 | if let Some(debugpy_dir) = | ||
| 59 | util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| { | ||
| 60 | file_name.starts_with("microsoft-debugpy-") | ||
| 61 | }) | ||
| 62 | .await | ||
| 63 | { | ||
| 64 | // TODO Debugger: Rename folder instead of moving all files to another folder | ||
| 65 | // We're doing unnecessary IO work right now | ||
| 66 | util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) | ||
| 67 | .await?; | ||
| 68 | } | ||
| 69 | |||
| 70 | Ok(()) | ||
| 71 | } | ||
| 72 | |||
| 73 | async fn get_installed_binary( | ||
| 74 | &self, | ||
| 75 | delegate: &dyn DapDelegate, | ||
| 76 | config: &DebugAdapterConfig, | ||
| 77 | user_installed_path: Option<PathBuf>, | ||
| 78 | cx: &mut AsyncApp, | ||
| 79 | ) -> Result<DebugAdapterBinary> { | ||
| 80 | const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; | ||
| 81 | |||
| 82 | let debugpy_dir = if let Some(user_installed_path) = user_installed_path { | ||
| 83 | user_installed_path | ||
| 84 | } else { | ||
| 85 | let adapter_path = paths::debug_adapters_dir().join(self.name()); | ||
| 86 | let file_name_prefix = format!("{}_", self.name()); | ||
| 87 | |||
| 88 | util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { | ||
| 89 | file_name.starts_with(&file_name_prefix) | ||
| 90 | }) | ||
| 91 | .await | ||
| 92 | .ok_or_else(|| anyhow!("Debugpy directory not found"))? | ||
| 93 | }; | ||
| 94 | |||
| 95 | let toolchain = delegate | ||
| 96 | .toolchain_store() | ||
| 97 | .active_toolchain( | ||
| 98 | delegate.worktree_id(), | ||
| 99 | language::LanguageName::new(Self::LANGUAGE_NAME), | ||
| 100 | cx, | ||
| 101 | ) | ||
| 102 | .await; | ||
| 103 | |||
| 104 | let python_path = if let Some(toolchain) = toolchain { | ||
| 105 | Some(toolchain.path.to_string()) | ||
| 106 | } else { | ||
| 107 | BINARY_NAMES | ||
| 108 | .iter() | ||
| 109 | .filter_map(|cmd| { | ||
| 110 | delegate | ||
| 111 | .which(OsStr::new(cmd)) | ||
| 112 | .map(|path| path.to_string_lossy().to_string()) | ||
| 113 | }) | ||
| 114 | .find(|_| true) | ||
| 115 | }; | ||
| 116 | |||
| 117 | Ok(DebugAdapterBinary { | ||
| 118 | command: python_path.ok_or(anyhow!("failed to find binary path for python"))?, | ||
| 119 | arguments: Some(vec![ | ||
| 120 | debugpy_dir.join(Self::ADAPTER_PATH).into(), | ||
| 121 | format!("--port={}", self.port).into(), | ||
| 122 | format!("--host={}", self.host).into(), | ||
| 123 | ]), | ||
| 124 | connection: Some(adapters::TcpArguments { | ||
| 125 | host: self.host, | ||
| 126 | port: self.port, | ||
| 127 | timeout: self.timeout, | ||
| 128 | }), | ||
| 129 | cwd: config.cwd.clone(), | ||
| 130 | envs: None, | ||
| 131 | }) | ||
| 132 | } | ||
| 133 | |||
| 134 | fn request_args(&self, config: &DebugAdapterConfig) -> Value { | ||
| 135 | json!({ | ||
| 136 | "program": config.program, | ||
| 137 | "subProcess": true, | ||
| 138 | "cwd": config.cwd, | ||
| 139 | "redirectOutput": true, | ||
| 140 | }) | ||
| 141 | } | ||
| 142 | } | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | [package] | ||
| 2 | name = "debugger_tools" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition.workspace = true | ||
| 5 | publish.workspace = true | ||
| 6 | license = "GPL-3.0-or-later" | ||
| 7 | |||
| 8 | [lints] | ||
| 9 | workspace = true | ||
| 10 | |||
| 11 | [lib] | ||
| 12 | path = "src/debugger_tools.rs" | ||
| 13 | doctest = false | ||
| 14 | |||
| 15 | [dependencies] | ||
| 16 | anyhow.workspace = true | ||
| 17 | dap.workspace = true | ||
| 18 | editor.workspace = true | ||
| 19 | futures.workspace = true | ||
| 20 | gpui.workspace = true | ||
| 21 | project.workspace = true | ||
| 22 | serde_json.workspace = true | ||
| 23 | settings.workspace = true | ||
| 24 | smol.workspace = true | ||
| 25 | util.workspace = true | ||
| 26 | workspace.workspace = true | ||
| @@ -0,0 +1 @@ | |||
| 1 | ../../LICENSE-GPL | ||
| @@ -0,0 +1,845 @@ | |||
| 1 | use dap::{ | ||
| 2 | client::SessionId, | ||
| 3 | debugger_settings::DebuggerSettings, | ||
| 4 | transport::{IoKind, LogKind}, | ||
| 5 | }; | ||
| 6 | use editor::{Editor, EditorEvent}; | ||
| 7 | use futures::{ | ||
| 8 | channel::mpsc::{unbounded, UnboundedSender}, | ||
| 9 | StreamExt, | ||
| 10 | }; | ||
| 11 | use gpui::{ | ||
| 12 | actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, | ||
| 13 | IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, | ||
| 14 | }; | ||
| 15 | use project::{ | ||
| 16 | debugger::{dap_store, session::Session}, | ||
| 17 | search::SearchQuery, | ||
| 18 | Project, | ||
| 19 | }; | ||
| 20 | use settings::Settings as _; | ||
| 21 | use std::{ | ||
| 22 | borrow::Cow, | ||
| 23 | collections::{HashMap, VecDeque}, | ||
| 24 | sync::Arc, | ||
| 25 | }; | ||
| 26 | use util::maybe; | ||
| 27 | use workspace::{ | ||
| 28 | item::Item, | ||
| 29 | searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, | ||
| 30 | ui::{h_flex, Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu}, | ||
| 31 | ToolbarItemEvent, ToolbarItemView, Workspace, | ||
| 32 | }; | ||
| 33 | |||
| 34 | struct DapLogView { | ||
| 35 | editor: Entity<Editor>, | ||
| 36 | focus_handle: FocusHandle, | ||
| 37 | log_store: Entity<LogStore>, | ||
| 38 | editor_subscriptions: Vec<Subscription>, | ||
| 39 | current_view: Option<(SessionId, LogKind)>, | ||
| 40 | project: Entity<Project>, | ||
| 41 | _subscriptions: Vec<Subscription>, | ||
| 42 | } | ||
| 43 | |||
| 44 | struct LogStore { | ||
| 45 | projects: HashMap<WeakEntity<Project>, ProjectState>, | ||
| 46 | debug_clients: HashMap<SessionId, DebugAdapterState>, | ||
| 47 | rpc_tx: UnboundedSender<(SessionId, IoKind, String)>, | ||
| 48 | adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>, | ||
| 49 | } | ||
| 50 | |||
| 51 | struct ProjectState { | ||
| 52 | _subscriptions: [gpui::Subscription; 2], | ||
| 53 | } | ||
| 54 | |||
| 55 | struct DebugAdapterState { | ||
| 56 | log_messages: VecDeque<String>, | ||
| 57 | rpc_messages: RpcMessages, | ||
| 58 | } | ||
| 59 | |||
| 60 | struct RpcMessages { | ||
| 61 | messages: VecDeque<String>, | ||
| 62 | last_message_kind: Option<MessageKind>, | ||
| 63 | } | ||
| 64 | |||
| 65 | impl RpcMessages { | ||
| 66 | const MESSAGE_QUEUE_LIMIT: usize = 255; | ||
| 67 | |||
| 68 | fn new() -> Self { | ||
| 69 | Self { | ||
| 70 | last_message_kind: None, | ||
| 71 | messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | const SEND: &str = "// Send"; | ||
| 77 | const RECEIVE: &str = "// Receive"; | ||
| 78 | |||
| 79 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 80 | enum MessageKind { | ||
| 81 | Send, | ||
| 82 | Receive, | ||
| 83 | } | ||
| 84 | |||
| 85 | impl MessageKind { | ||
| 86 | fn label(&self) -> &'static str { | ||
| 87 | match self { | ||
| 88 | Self::Send => SEND, | ||
| 89 | Self::Receive => RECEIVE, | ||
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | impl DebugAdapterState { | ||
| 95 | fn new() -> Self { | ||
| 96 | Self { | ||
| 97 | log_messages: VecDeque::new(), | ||
| 98 | rpc_messages: RpcMessages::new(), | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | impl LogStore { | ||
| 104 | fn new(cx: &Context<Self>) -> Self { | ||
| 105 | let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>(); | ||
| 106 | cx.spawn(|this, mut cx| async move { | ||
| 107 | while let Some((client_id, io_kind, message)) = rpc_rx.next().await { | ||
| 108 | if let Some(this) = this.upgrade() { | ||
| 109 | this.update(&mut cx, |this, cx| { | ||
| 110 | this.on_rpc_log(client_id, io_kind, &message, cx); | ||
| 111 | })?; | ||
| 112 | } | ||
| 113 | |||
| 114 | smol::future::yield_now().await; | ||
| 115 | } | ||
| 116 | anyhow::Ok(()) | ||
| 117 | }) | ||
| 118 | .detach_and_log_err(cx); | ||
| 119 | |||
| 120 | let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>(); | ||
| 121 | cx.spawn(|this, mut cx| async move { | ||
| 122 | while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await { | ||
| 123 | if let Some(this) = this.upgrade() { | ||
| 124 | this.update(&mut cx, |this, cx| { | ||
| 125 | this.on_adapter_log(client_id, io_kind, &message, cx); | ||
| 126 | })?; | ||
| 127 | } | ||
| 128 | |||
| 129 | smol::future::yield_now().await; | ||
| 130 | } | ||
| 131 | anyhow::Ok(()) | ||
| 132 | }) | ||
| 133 | .detach_and_log_err(cx); | ||
| 134 | Self { | ||
| 135 | rpc_tx, | ||
| 136 | adapter_log_tx, | ||
| 137 | projects: HashMap::new(), | ||
| 138 | debug_clients: HashMap::new(), | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | fn on_rpc_log( | ||
| 143 | &mut self, | ||
| 144 | client_id: SessionId, | ||
| 145 | io_kind: IoKind, | ||
| 146 | message: &str, | ||
| 147 | cx: &mut Context<Self>, | ||
| 148 | ) { | ||
| 149 | self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); | ||
| 150 | } | ||
| 151 | |||
| 152 | fn on_adapter_log( | ||
| 153 | &mut self, | ||
| 154 | client_id: SessionId, | ||
| 155 | io_kind: IoKind, | ||
| 156 | message: &str, | ||
| 157 | cx: &mut Context<Self>, | ||
| 158 | ) { | ||
| 159 | self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); | ||
| 160 | } | ||
| 161 | |||
| 162 | pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { | ||
| 163 | let weak_project = project.downgrade(); | ||
| 164 | self.projects.insert( | ||
| 165 | project.downgrade(), | ||
| 166 | ProjectState { | ||
| 167 | _subscriptions: [ | ||
| 168 | cx.observe_release(project, move |this, _, _| { | ||
| 169 | this.projects.remove(&weak_project); | ||
| 170 | }), | ||
| 171 | cx.subscribe( | ||
| 172 | &project.read(cx).dap_store(), | ||
| 173 | |this, dap_store, event, cx| match event { | ||
| 174 | dap_store::DapStoreEvent::DebugClientStarted(session_id) => { | ||
| 175 | let session = dap_store.read(cx).session_by_id(session_id); | ||
| 176 | if let Some(session) = session { | ||
| 177 | this.add_debug_client(*session_id, session, cx); | ||
| 178 | } | ||
| 179 | } | ||
| 180 | dap_store::DapStoreEvent::DebugClientShutdown(session_id) => { | ||
| 181 | this.remove_debug_client(*session_id, cx); | ||
| 182 | } | ||
| 183 | |||
| 184 | _ => {} | ||
| 185 | }, | ||
| 186 | ), | ||
| 187 | ], | ||
| 188 | }, | ||
| 189 | ); | ||
| 190 | } | ||
| 191 | |||
| 192 | fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { | ||
| 193 | self.debug_clients.get_mut(&id) | ||
| 194 | } | ||
| 195 | |||
| 196 | fn add_debug_client_message( | ||
| 197 | &mut self, | ||
| 198 | id: SessionId, | ||
| 199 | io_kind: IoKind, | ||
| 200 | message: String, | ||
| 201 | cx: &mut Context<Self>, | ||
| 202 | ) { | ||
| 203 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { | ||
| 204 | return; | ||
| 205 | }; | ||
| 206 | |||
| 207 | let kind = match io_kind { | ||
| 208 | IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, | ||
| 209 | IoKind::StdIn => MessageKind::Send, | ||
| 210 | }; | ||
| 211 | |||
| 212 | let rpc_messages = &mut debug_client_state.rpc_messages; | ||
| 213 | if rpc_messages.last_message_kind != Some(kind) { | ||
| 214 | Self::add_debug_client_entry( | ||
| 215 | &mut rpc_messages.messages, | ||
| 216 | id, | ||
| 217 | kind.label().to_string(), | ||
| 218 | LogKind::Rpc, | ||
| 219 | cx, | ||
| 220 | ); | ||
| 221 | rpc_messages.last_message_kind = Some(kind); | ||
| 222 | } | ||
| 223 | Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); | ||
| 224 | |||
| 225 | cx.notify(); | ||
| 226 | } | ||
| 227 | |||
| 228 | fn add_debug_client_log( | ||
| 229 | &mut self, | ||
| 230 | id: SessionId, | ||
| 231 | io_kind: IoKind, | ||
| 232 | message: String, | ||
| 233 | cx: &mut Context<Self>, | ||
| 234 | ) { | ||
| 235 | let Some(debug_client_state) = self.get_debug_adapter_state(id) else { | ||
| 236 | return; | ||
| 237 | }; | ||
| 238 | |||
| 239 | let message = match io_kind { | ||
| 240 | IoKind::StdErr => { | ||
| 241 | let mut message = message.clone(); | ||
| 242 | message.insert_str(0, "stderr: "); | ||
| 243 | message | ||
| 244 | } | ||
| 245 | _ => message, | ||
| 246 | }; | ||
| 247 | |||
| 248 | Self::add_debug_client_entry( | ||
| 249 | &mut debug_client_state.log_messages, | ||
| 250 | id, | ||
| 251 | message, | ||
| 252 | LogKind::Adapter, | ||
| 253 | cx, | ||
| 254 | ); | ||
| 255 | cx.notify(); | ||
| 256 | } | ||
| 257 | |||
| 258 | fn add_debug_client_entry( | ||
| 259 | log_lines: &mut VecDeque<String>, | ||
| 260 | id: SessionId, | ||
| 261 | message: String, | ||
| 262 | kind: LogKind, | ||
| 263 | cx: &mut Context<Self>, | ||
| 264 | ) { | ||
| 265 | while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { | ||
| 266 | log_lines.pop_front(); | ||
| 267 | } | ||
| 268 | |||
| 269 | let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages; | ||
| 270 | |||
| 271 | let entry = if format_messages { | ||
| 272 | maybe!({ | ||
| 273 | serde_json::to_string_pretty::<serde_json::Value>( | ||
| 274 | &serde_json::from_str(&message).ok()?, | ||
| 275 | ) | ||
| 276 | .ok() | ||
| 277 | }) | ||
| 278 | .unwrap_or(message) | ||
| 279 | } else { | ||
| 280 | message | ||
| 281 | }; | ||
| 282 | log_lines.push_back(entry.clone()); | ||
| 283 | |||
| 284 | cx.emit(Event::NewLogEntry { id, entry, kind }); | ||
| 285 | } | ||
| 286 | |||
| 287 | fn add_debug_client( | ||
| 288 | &mut self, | ||
| 289 | client_id: SessionId, | ||
| 290 | client: Entity<Session>, | ||
| 291 | cx: &App, | ||
| 292 | ) -> Option<&mut DebugAdapterState> { | ||
| 293 | let client_state = self | ||
| 294 | .debug_clients | ||
| 295 | .entry(client_id) | ||
| 296 | .or_insert_with(DebugAdapterState::new); | ||
| 297 | |||
| 298 | let io_tx = self.rpc_tx.clone(); | ||
| 299 | |||
| 300 | let client = client.read(cx).adapter_client()?; | ||
| 301 | client.add_log_handler( | ||
| 302 | move |io_kind, message| { | ||
| 303 | io_tx | ||
| 304 | .unbounded_send((client_id, io_kind, message.to_string())) | ||
| 305 | .ok(); | ||
| 306 | }, | ||
| 307 | LogKind::Rpc, | ||
| 308 | ); | ||
| 309 | |||
| 310 | let log_io_tx = self.adapter_log_tx.clone(); | ||
| 311 | client.add_log_handler( | ||
| 312 | move |io_kind, message| { | ||
| 313 | log_io_tx | ||
| 314 | .unbounded_send((client_id, io_kind, message.to_string())) | ||
| 315 | .ok(); | ||
| 316 | }, | ||
| 317 | LogKind::Adapter, | ||
| 318 | ); | ||
| 319 | |||
| 320 | Some(client_state) | ||
| 321 | } | ||
| 322 | |||
| 323 | fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) { | ||
| 324 | self.debug_clients.remove(&client_id); | ||
| 325 | cx.notify(); | ||
| 326 | } | ||
| 327 | |||
| 328 | fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> { | ||
| 329 | Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) | ||
| 330 | } | ||
| 331 | |||
| 332 | fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> { | ||
| 333 | Some( | ||
| 334 | &mut self | ||
| 335 | .debug_clients | ||
| 336 | .get_mut(&client_id)? | ||
| 337 | .rpc_messages | ||
| 338 | .messages, | ||
| 339 | ) | ||
| 340 | } | ||
| 341 | } | ||
| 342 | |||
| 343 | pub struct DapLogToolbarItemView { | ||
| 344 | log_view: Option<Entity<DapLogView>>, | ||
| 345 | } | ||
| 346 | |||
| 347 | impl DapLogToolbarItemView { | ||
| 348 | pub fn new() -> Self { | ||
| 349 | Self { log_view: None } | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | impl Render for DapLogToolbarItemView { | ||
| 354 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 355 | let Some(log_view) = self.log_view.clone() else { | ||
| 356 | return Empty.into_any_element(); | ||
| 357 | }; | ||
| 358 | |||
| 359 | let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| { | ||
| 360 | ( | ||
| 361 | log_view.menu_items(cx).unwrap_or_default(), | ||
| 362 | log_view.current_view.map(|(client_id, _)| client_id), | ||
| 363 | ) | ||
| 364 | }); | ||
| 365 | |||
| 366 | let current_client = current_client_id.and_then(|current_client_id| { | ||
| 367 | menu_rows | ||
| 368 | .iter() | ||
| 369 | .find(|row| row.client_id == current_client_id) | ||
| 370 | }); | ||
| 371 | |||
| 372 | let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") | ||
| 373 | .anchor(gpui::Corner::TopLeft) | ||
| 374 | .trigger(Button::new( | ||
| 375 | "debug_client_menu_header", | ||
| 376 | current_client | ||
| 377 | .map(|sub_item| { | ||
| 378 | Cow::Owned(format!( | ||
| 379 | "{} ({}) - {}", | ||
| 380 | sub_item.client_name, | ||
| 381 | sub_item.client_id.0, | ||
| 382 | match sub_item.selected_entry { | ||
| 383 | LogKind::Adapter => ADAPTER_LOGS, | ||
| 384 | LogKind::Rpc => RPC_MESSAGES, | ||
| 385 | } | ||
| 386 | )) | ||
| 387 | }) | ||
| 388 | .unwrap_or_else(|| "No adapter selected".into()), | ||
| 389 | )) | ||
| 390 | .menu(move |mut window, cx| { | ||
| 391 | let log_view = log_view.clone(); | ||
| 392 | let menu_rows = menu_rows.clone(); | ||
| 393 | ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { | ||
| 394 | for row in menu_rows.into_iter() { | ||
| 395 | menu = menu.custom_row(move |_window, _cx| { | ||
| 396 | div() | ||
| 397 | .w_full() | ||
| 398 | .pl_2() | ||
| 399 | .child( | ||
| 400 | Label::new( | ||
| 401 | format!("{}. {}", row.client_id.0, row.client_name,), | ||
| 402 | ) | ||
| 403 | .color(workspace::ui::Color::Muted), | ||
| 404 | ) | ||
| 405 | .into_any_element() | ||
| 406 | }); | ||
| 407 | |||
| 408 | if row.has_adapter_logs { | ||
| 409 | menu = menu.custom_entry( | ||
| 410 | move |_window, _cx| { | ||
| 411 | div() | ||
| 412 | .w_full() | ||
| 413 | .pl_4() | ||
| 414 | .child(Label::new(ADAPTER_LOGS)) | ||
| 415 | .into_any_element() | ||
| 416 | }, | ||
| 417 | window.handler_for(&log_view, move |view, window, cx| { | ||
| 418 | view.show_log_messages_for_adapter(row.client_id, window, cx); | ||
| 419 | }), | ||
| 420 | ); | ||
| 421 | } | ||
| 422 | |||
| 423 | menu = menu.custom_entry( | ||
| 424 | move |_window, _cx| { | ||
| 425 | div() | ||
| 426 | .w_full() | ||
| 427 | .pl_4() | ||
| 428 | .child(Label::new(RPC_MESSAGES)) | ||
| 429 | .into_any_element() | ||
| 430 | }, | ||
| 431 | window.handler_for(&log_view, move |view, window, cx| { | ||
| 432 | view.show_rpc_trace_for_server(row.client_id, window, cx); | ||
| 433 | }), | ||
| 434 | ); | ||
| 435 | } | ||
| 436 | |||
| 437 | menu | ||
| 438 | }) | ||
| 439 | .into() | ||
| 440 | }); | ||
| 441 | |||
| 442 | h_flex() | ||
| 443 | .size_full() | ||
| 444 | .child(dap_menu) | ||
| 445 | .child( | ||
| 446 | div() | ||
| 447 | .child( | ||
| 448 | Button::new("clear_log_button", "Clear").on_click(cx.listener( | ||
| 449 | |this, _, window, cx| { | ||
| 450 | if let Some(log_view) = this.log_view.as_ref() { | ||
| 451 | log_view.update(cx, |log_view, cx| { | ||
| 452 | log_view.editor.update(cx, |editor, cx| { | ||
| 453 | editor.set_read_only(false); | ||
| 454 | editor.clear(window, cx); | ||
| 455 | editor.set_read_only(true); | ||
| 456 | }); | ||
| 457 | }) | ||
| 458 | } | ||
| 459 | }, | ||
| 460 | )), | ||
| 461 | ) | ||
| 462 | .ml_2(), | ||
| 463 | ) | ||
| 464 | .into_any_element() | ||
| 465 | } | ||
| 466 | } | ||
| 467 | |||
| 468 | impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {} | ||
| 469 | |||
| 470 | impl ToolbarItemView for DapLogToolbarItemView { | ||
| 471 | fn set_active_pane_item( | ||
| 472 | &mut self, | ||
| 473 | active_pane_item: Option<&dyn workspace::item::ItemHandle>, | ||
| 474 | _window: &mut Window, | ||
| 475 | cx: &mut Context<Self>, | ||
| 476 | ) -> workspace::ToolbarItemLocation { | ||
| 477 | if let Some(item) = active_pane_item { | ||
| 478 | if let Some(log_view) = item.downcast::<DapLogView>() { | ||
| 479 | self.log_view = Some(log_view.clone()); | ||
| 480 | return workspace::ToolbarItemLocation::PrimaryLeft; | ||
| 481 | } | ||
| 482 | } | ||
| 483 | self.log_view = None; | ||
| 484 | |||
| 485 | cx.notify(); | ||
| 486 | |||
| 487 | workspace::ToolbarItemLocation::Hidden | ||
| 488 | } | ||
| 489 | } | ||
| 490 | |||
| 491 | impl DapLogView { | ||
| 492 | pub fn new( | ||
| 493 | project: Entity<Project>, | ||
| 494 | log_store: Entity<LogStore>, | ||
| 495 | window: &mut Window, | ||
| 496 | cx: &mut Context<Self>, | ||
| 497 | ) -> Self { | ||
| 498 | let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx); | ||
| 499 | |||
| 500 | let focus_handle = cx.focus_handle(); | ||
| 501 | |||
| 502 | let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event { | ||
| 503 | Event::NewLogEntry { id, entry, kind } => { | ||
| 504 | if log_view.current_view == Some((*id, *kind)) { | ||
| 505 | log_view.editor.update(cx, |editor, cx| { | ||
| 506 | editor.set_read_only(false); | ||
| 507 | let last_point = editor.buffer().read(cx).len(cx); | ||
| 508 | editor.edit( | ||
| 509 | vec![ | ||
| 510 | (last_point..last_point, entry.trim()), | ||
| 511 | (last_point..last_point, "\n"), | ||
| 512 | ], | ||
| 513 | cx, | ||
| 514 | ); | ||
| 515 | editor.set_read_only(true); | ||
| 516 | }); | ||
| 517 | } | ||
| 518 | } | ||
| 519 | }); | ||
| 520 | |||
| 521 | Self { | ||
| 522 | editor, | ||
| 523 | focus_handle, | ||
| 524 | project, | ||
| 525 | log_store, | ||
| 526 | editor_subscriptions, | ||
| 527 | current_view: None, | ||
| 528 | _subscriptions: vec![events_subscriptions], | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | fn editor_for_logs( | ||
| 533 | log_contents: String, | ||
| 534 | window: &mut Window, | ||
| 535 | cx: &mut Context<Self>, | ||
| 536 | ) -> (Entity<Editor>, Vec<Subscription>) { | ||
| 537 | let editor = cx.new(|cx| { | ||
| 538 | let mut editor = Editor::multi_line(window, cx); | ||
| 539 | editor.set_text(log_contents, window, cx); | ||
| 540 | editor.move_to_end(&editor::actions::MoveToEnd, window, cx); | ||
| 541 | editor.set_show_code_actions(false, cx); | ||
| 542 | editor.set_show_breakpoints(false, cx); | ||
| 543 | editor.set_show_git_diff_gutter(false, cx); | ||
| 544 | editor.set_show_runnables(false, cx); | ||
| 545 | editor.set_input_enabled(false); | ||
| 546 | editor.set_use_autoclose(false); | ||
| 547 | editor.set_read_only(true); | ||
| 548 | editor.set_show_edit_predictions(Some(false), window, cx); | ||
| 549 | editor | ||
| 550 | }); | ||
| 551 | let editor_subscription = cx.subscribe( | ||
| 552 | &editor, | ||
| 553 | |_, _, event: &EditorEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()), | ||
| 554 | ); | ||
| 555 | let search_subscription = cx.subscribe( | ||
| 556 | &editor, | ||
| 557 | |_, _, event: &SearchEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()), | ||
| 558 | ); | ||
| 559 | (editor, vec![editor_subscription, search_subscription]) | ||
| 560 | } | ||
| 561 | |||
| 562 | fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> { | ||
| 563 | let mut menu_items = self | ||
| 564 | .project | ||
| 565 | .read(cx) | ||
| 566 | .dap_store() | ||
| 567 | .read(cx) | ||
| 568 | .sessions() | ||
| 569 | .filter_map(|client| { | ||
| 570 | let client = client.read(cx).adapter_client()?; | ||
| 571 | Some(DapMenuItem { | ||
| 572 | client_id: client.id(), | ||
| 573 | client_name: client.name().0.as_ref().into(), | ||
| 574 | has_adapter_logs: client.has_adapter_logs(), | ||
| 575 | selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), | ||
| 576 | }) | ||
| 577 | }) | ||
| 578 | .collect::<Vec<_>>(); | ||
| 579 | menu_items.sort_by_key(|item| item.client_id.0); | ||
| 580 | Some(menu_items) | ||
| 581 | } | ||
| 582 | |||
| 583 | fn show_rpc_trace_for_server( | ||
| 584 | &mut self, | ||
| 585 | client_id: SessionId, | ||
| 586 | window: &mut Window, | ||
| 587 | cx: &mut Context<Self>, | ||
| 588 | ) { | ||
| 589 | let rpc_log = self.log_store.update(cx, |log_store, _| { | ||
| 590 | log_store | ||
| 591 | .rpc_messages_for_client(client_id) | ||
| 592 | .map(|state| log_contents(&state)) | ||
| 593 | }); | ||
| 594 | if let Some(rpc_log) = rpc_log { | ||
| 595 | self.current_view = Some((client_id, LogKind::Rpc)); | ||
| 596 | let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); | ||
| 597 | let language = self.project.read(cx).languages().language_for_name("JSON"); | ||
| 598 | editor | ||
| 599 | .read(cx) | ||
| 600 | .buffer() | ||
| 601 | .read(cx) | ||
| 602 | .as_singleton() | ||
| 603 | .expect("log buffer should be a singleton") | ||
| 604 | .update(cx, |_, cx| { | ||
| 605 | cx.spawn({ | ||
| 606 | let buffer = cx.entity(); | ||
| 607 | |_, mut cx| async move { | ||
| 608 | let language = language.await.ok(); | ||
| 609 | buffer.update(&mut cx, |buffer, cx| { | ||
| 610 | buffer.set_language(language, cx); | ||
| 611 | }) | ||
| 612 | } | ||
| 613 | }) | ||
| 614 | .detach_and_log_err(cx); | ||
| 615 | }); | ||
| 616 | |||
| 617 | self.editor = editor; | ||
| 618 | self.editor_subscriptions = editor_subscriptions; | ||
| 619 | cx.notify(); | ||
| 620 | } | ||
| 621 | |||
| 622 | cx.focus_self(window); | ||
| 623 | } | ||
| 624 | |||
| 625 | fn show_log_messages_for_adapter( | ||
| 626 | &mut self, | ||
| 627 | client_id: SessionId, | ||
| 628 | window: &mut Window, | ||
| 629 | cx: &mut Context<Self>, | ||
| 630 | ) { | ||
| 631 | let message_log = self.log_store.update(cx, |log_store, _| { | ||
| 632 | log_store | ||
| 633 | .log_messages_for_client(client_id) | ||
| 634 | .map(|state| log_contents(&state)) | ||
| 635 | }); | ||
| 636 | if let Some(message_log) = message_log { | ||
| 637 | self.current_view = Some((client_id, LogKind::Adapter)); | ||
| 638 | let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); | ||
| 639 | editor | ||
| 640 | .read(cx) | ||
| 641 | .buffer() | ||
| 642 | .read(cx) | ||
| 643 | .as_singleton() | ||
| 644 | .expect("log buffer should be a singleton"); | ||
| 645 | |||
| 646 | self.editor = editor; | ||
| 647 | self.editor_subscriptions = editor_subscriptions; | ||
| 648 | cx.notify(); | ||
| 649 | } | ||
| 650 | |||
| 651 | cx.focus_self(window); | ||
| 652 | } | ||
| 653 | } | ||
| 654 | |||
| 655 | fn log_contents(lines: &VecDeque<String>) -> String { | ||
| 656 | let (a, b) = lines.as_slices(); | ||
| 657 | let a = a.iter().map(move |v| v.as_ref()); | ||
| 658 | let b = b.iter().map(move |v| v.as_ref()); | ||
| 659 | a.chain(b).fold(String::new(), |mut acc, el| { | ||
| 660 | acc.push_str(el); | ||
| 661 | acc.push('\n'); | ||
| 662 | acc | ||
| 663 | }) | ||
| 664 | } | ||
| 665 | |||
| 666 | #[derive(Clone, PartialEq)] | ||
| 667 | pub(crate) struct DapMenuItem { | ||
| 668 | pub client_id: SessionId, | ||
| 669 | pub client_name: String, | ||
| 670 | pub has_adapter_logs: bool, | ||
| 671 | pub selected_entry: LogKind, | ||
| 672 | } | ||
| 673 | |||
| 674 | const ADAPTER_LOGS: &str = "Adapter Logs"; | ||
| 675 | const RPC_MESSAGES: &str = "RPC Messages"; | ||
| 676 | |||
| 677 | impl Render for DapLogView { | ||
| 678 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 679 | self.editor.update(cx, |editor, cx| { | ||
| 680 | editor.render(window, cx).into_any_element() | ||
| 681 | }) | ||
| 682 | } | ||
| 683 | } | ||
| 684 | |||
| 685 | actions!(debug, [OpenDebuggerAdapterLogs]); | ||
| 686 | |||
| 687 | pub fn init(cx: &mut App) { | ||
| 688 | let log_store = cx.new(|cx| LogStore::new(cx)); | ||
| 689 | |||
| 690 | cx.observe_new(move |workspace: &mut Workspace, window, cx| { | ||
| 691 | let Some(_window) = window else { | ||
| 692 | return; | ||
| 693 | }; | ||
| 694 | |||
| 695 | let project = workspace.project(); | ||
| 696 | if project.read(cx).is_local() { | ||
| 697 | log_store.update(cx, |store, cx| { | ||
| 698 | store.add_project(project, cx); | ||
| 699 | }); | ||
| 700 | } | ||
| 701 | |||
| 702 | let log_store = log_store.clone(); | ||
| 703 | workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| { | ||
| 704 | let project = workspace.project().read(cx); | ||
| 705 | if project.is_local() { | ||
| 706 | workspace.add_item_to_active_pane( | ||
| 707 | Box::new(cx.new(|cx| { | ||
| 708 | DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx) | ||
| 709 | })), | ||
| 710 | None, | ||
| 711 | true, | ||
| 712 | window, | ||
| 713 | cx, | ||
| 714 | ); | ||
| 715 | } | ||
| 716 | }); | ||
| 717 | }) | ||
| 718 | .detach(); | ||
| 719 | } | ||
| 720 | |||
| 721 | impl Item for DapLogView { | ||
| 722 | type Event = EditorEvent; | ||
| 723 | |||
| 724 | fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { | ||
| 725 | Editor::to_item_events(event, f) | ||
| 726 | } | ||
| 727 | |||
| 728 | fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> { | ||
| 729 | Some("DAP Logs".into()) | ||
| 730 | } | ||
| 731 | |||
| 732 | fn telemetry_event_text(&self) -> Option<&'static str> { | ||
| 733 | None | ||
| 734 | } | ||
| 735 | |||
| 736 | fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> { | ||
| 737 | Some(Box::new(handle.clone())) | ||
| 738 | } | ||
| 739 | } | ||
| 740 | |||
| 741 | impl SearchableItem for DapLogView { | ||
| 742 | type Match = <Editor as SearchableItem>::Match; | ||
| 743 | |||
| 744 | fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) { | ||
| 745 | self.editor.update(cx, |e, cx| e.clear_matches(window, cx)) | ||
| 746 | } | ||
| 747 | |||
| 748 | fn update_matches( | ||
| 749 | &mut self, | ||
| 750 | matches: &[Self::Match], | ||
| 751 | window: &mut Window, | ||
| 752 | cx: &mut Context<Self>, | ||
| 753 | ) { | ||
| 754 | self.editor | ||
| 755 | .update(cx, |e, cx| e.update_matches(matches, window, cx)) | ||
| 756 | } | ||
| 757 | |||
| 758 | fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String { | ||
| 759 | self.editor | ||
| 760 | .update(cx, |e, cx| e.query_suggestion(window, cx)) | ||
| 761 | } | ||
| 762 | |||
| 763 | fn activate_match( | ||
| 764 | &mut self, | ||
| 765 | index: usize, | ||
| 766 | matches: &[Self::Match], | ||
| 767 | window: &mut Window, | ||
| 768 | cx: &mut Context<Self>, | ||
| 769 | ) { | ||
| 770 | self.editor | ||
| 771 | .update(cx, |e, cx| e.activate_match(index, matches, window, cx)) | ||
| 772 | } | ||
| 773 | |||
| 774 | fn select_matches( | ||
| 775 | &mut self, | ||
| 776 | matches: &[Self::Match], | ||
| 777 | window: &mut Window, | ||
| 778 | cx: &mut Context<Self>, | ||
| 779 | ) { | ||
| 780 | self.editor | ||
| 781 | .update(cx, |e, cx| e.select_matches(matches, window, cx)) | ||
| 782 | } | ||
| 783 | |||
| 784 | fn find_matches( | ||
| 785 | &mut self, | ||
| 786 | query: Arc<project::search::SearchQuery>, | ||
| 787 | window: &mut Window, | ||
| 788 | cx: &mut Context<Self>, | ||
| 789 | ) -> gpui::Task<Vec<Self::Match>> { | ||
| 790 | self.editor | ||
| 791 | .update(cx, |e, cx| e.find_matches(query, window, cx)) | ||
| 792 | } | ||
| 793 | |||
| 794 | fn replace( | ||
| 795 | &mut self, | ||
| 796 | _: &Self::Match, | ||
| 797 | _: &SearchQuery, | ||
| 798 | _window: &mut Window, | ||
| 799 | _: &mut Context<Self>, | ||
| 800 | ) { | ||
| 801 | // Since DAP Log is read-only, it doesn't make sense to support replace operation. | ||
| 802 | } | ||
| 803 | |||
| 804 | fn supported_options(&self) -> workspace::searchable::SearchOptions { | ||
| 805 | workspace::searchable::SearchOptions { | ||
| 806 | case: true, | ||
| 807 | word: true, | ||
| 808 | regex: true, | ||
| 809 | find_in_results: true, | ||
| 810 | // DAP log is read-only. | ||
| 811 | replacement: false, | ||
| 812 | selection: false, | ||
| 813 | } | ||
| 814 | } | ||
| 815 | fn active_match_index( | ||
| 816 | &mut self, | ||
| 817 | direction: Direction, | ||
| 818 | matches: &[Self::Match], | ||
| 819 | window: &mut Window, | ||
| 820 | cx: &mut Context<Self>, | ||
| 821 | ) -> Option<usize> { | ||
| 822 | self.editor.update(cx, |e, cx| { | ||
| 823 | e.active_match_index(direction, matches, window, cx) | ||
| 824 | }) | ||
| 825 | } | ||
| 826 | } | ||
| 827 | |||
| 828 | impl Focusable for DapLogView { | ||
| 829 | fn focus_handle(&self, _cx: &App) -> FocusHandle { | ||
| 830 | self.focus_handle.clone() | ||
| 831 | } | ||
| 832 | } | ||
| 833 | |||
| 834 | pub enum Event { | ||
| 835 | NewLogEntry { | ||
| 836 | id: SessionId, | ||
| 837 | entry: String, | ||
| 838 | kind: LogKind, | ||
| 839 | }, | ||
| 840 | } | ||
| 841 | |||
| 842 | impl EventEmitter<Event> for LogStore {} | ||
| 843 | impl EventEmitter<Event> for DapLogView {} | ||
| 844 | impl EventEmitter<EditorEvent> for DapLogView {} | ||
| 845 | impl EventEmitter<SearchEvent> for DapLogView {} | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | mod dap_log; | ||
| 2 | pub use dap_log::*; | ||
| 3 | |||
| 4 | use gpui::App; | ||
| 5 | |||
| 6 | pub fn init(cx: &mut App) { | ||
| 7 | dap_log::init(cx); | ||
| 8 | } | ||
| @@ -0,0 +1,58 @@ | |||
| 1 | [package] | ||
| 2 | name = "debugger_ui" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition.workspace = true | ||
| 5 | publish.workspace = true | ||
| 6 | license = "GPL-3.0-or-later" | ||
| 7 | |||
| 8 | [lints] | ||
| 9 | workspace = true | ||
| 10 | |||
| 11 | [features] | ||
| 12 | test-support = [ | ||
| 13 | "dap/test-support", | ||
| 14 | "editor/test-support", | ||
| 15 | "gpui/test-support", | ||
| 16 | "project/test-support", | ||
| 17 | "util/test-support", | ||
| 18 | "workspace/test-support", | ||
| 19 | ] | ||
| 20 | |||
| 21 | [dependencies] | ||
| 22 | anyhow.workspace = true | ||
| 23 | client.workspace = true | ||
| 24 | collections.workspace = true | ||
| 25 | command_palette_hooks.workspace = true | ||
| 26 | dap.workspace = true | ||
| 27 | editor.workspace = true | ||
| 28 | feature_flags.workspace = true | ||
| 29 | futures.workspace = true | ||
| 30 | fuzzy.workspace = true | ||
| 31 | gpui.workspace = true | ||
| 32 | language.workspace = true | ||
| 33 | log.workspace = true | ||
| 34 | menu.workspace = true | ||
| 35 | picker.workspace = true | ||
| 36 | pretty_assertions.workspace = true | ||
| 37 | project.workspace = true | ||
| 38 | rpc.workspace = true | ||
| 39 | serde.workspace = true | ||
| 40 | serde_json.workspace = true | ||
| 41 | settings.workspace = true | ||
| 42 | sysinfo.workspace = true | ||
| 43 | task.workspace = true | ||
| 44 | terminal_view.workspace = true | ||
| 45 | theme.workspace = true | ||
| 46 | ui.workspace = true | ||
| 47 | util.workspace = true | ||
| 48 | workspace.workspace = true | ||
| 49 | |||
| 50 | [dev-dependencies] | ||
| 51 | dap = { workspace = true, features = ["test-support"] } | ||
| 52 | editor = { workspace = true, features = ["test-support"] } | ||
| 53 | env_logger.workspace = true | ||
| 54 | gpui = { workspace = true, features = ["test-support"] } | ||
| 55 | project = { workspace = true, features = ["test-support"] } | ||
| 56 | unindent.workspace = true | ||
| 57 | util = { workspace = true, features = ["test-support"] } | ||
| 58 | workspace = { workspace = true, features = ["test-support"] } | ||
| @@ -0,0 +1 @@ | |||
| 1 | ../../LICENSE-GPL | ||
| @@ -0,0 +1,293 @@ | |||
| 1 | use dap::DebugRequestType; | ||
| 2 | use fuzzy::{StringMatch, StringMatchCandidate}; | ||
| 3 | use gpui::Subscription; | ||
| 4 | use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; | ||
| 5 | use picker::{Picker, PickerDelegate}; | ||
| 6 | use project::debugger::attach_processes; | ||
| 7 | |||
| 8 | use std::sync::Arc; | ||
| 9 | use sysinfo::System; | ||
| 10 | use ui::{prelude::*, Context, Tooltip}; | ||
| 11 | use ui::{ListItem, ListItemSpacing}; | ||
| 12 | use util::debug_panic; | ||
| 13 | use workspace::ModalView; | ||
| 14 | |||
| 15 | #[derive(Debug, Clone)] | ||
| 16 | struct Candidate { | ||
| 17 | pid: u32, | ||
| 18 | name: String, | ||
| 19 | command: Vec<String>, | ||
| 20 | } | ||
| 21 | |||
| 22 | pub(crate) struct AttachModalDelegate { | ||
| 23 | selected_index: usize, | ||
| 24 | matches: Vec<StringMatch>, | ||
| 25 | placeholder_text: Arc<str>, | ||
| 26 | project: Entity<project::Project>, | ||
| 27 | debug_config: task::DebugAdapterConfig, | ||
| 28 | candidates: Option<Vec<Candidate>>, | ||
| 29 | } | ||
| 30 | |||
| 31 | impl AttachModalDelegate { | ||
| 32 | pub fn new(project: Entity<project::Project>, debug_config: task::DebugAdapterConfig) -> Self { | ||
| 33 | Self { | ||
| 34 | project, | ||
| 35 | debug_config, | ||
| 36 | candidates: None, | ||
| 37 | selected_index: 0, | ||
| 38 | matches: Vec::default(), | ||
| 39 | placeholder_text: Arc::from("Select the process you want to attach the debugger to"), | ||
| 40 | } | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | pub struct AttachModal { | ||
| 45 | _subscription: Subscription, | ||
| 46 | pub(crate) picker: Entity<Picker<AttachModalDelegate>>, | ||
| 47 | } | ||
| 48 | |||
| 49 | impl AttachModal { | ||
| 50 | pub fn new( | ||
| 51 | project: Entity<project::Project>, | ||
| 52 | debug_config: task::DebugAdapterConfig, | ||
| 53 | window: &mut Window, | ||
| 54 | cx: &mut Context<Self>, | ||
| 55 | ) -> Self { | ||
| 56 | let picker = cx.new(|cx| { | ||
| 57 | Picker::uniform_list(AttachModalDelegate::new(project, debug_config), window, cx) | ||
| 58 | }); | ||
| 59 | Self { | ||
| 60 | _subscription: cx.subscribe(&picker, |_, _, _, cx| { | ||
| 61 | cx.emit(DismissEvent); | ||
| 62 | }), | ||
| 63 | picker, | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | impl Render for AttachModal { | ||
| 69 | fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement { | ||
| 70 | v_flex() | ||
| 71 | .key_context("AttachModal") | ||
| 72 | .w(rems(34.)) | ||
| 73 | .child(self.picker.clone()) | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | impl EventEmitter<DismissEvent> for AttachModal {} | ||
| 78 | |||
| 79 | impl Focusable for AttachModal { | ||
| 80 | fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { | ||
| 81 | self.picker.read(cx).focus_handle(cx) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | impl ModalView for AttachModal {} | ||
| 86 | |||
| 87 | impl PickerDelegate for AttachModalDelegate { | ||
| 88 | type ListItem = ListItem; | ||
| 89 | |||
| 90 | fn match_count(&self) -> usize { | ||
| 91 | self.matches.len() | ||
| 92 | } | ||
| 93 | |||
| 94 | fn selected_index(&self) -> usize { | ||
| 95 | self.selected_index | ||
| 96 | } | ||
| 97 | |||
| 98 | fn set_selected_index( | ||
| 99 | &mut self, | ||
| 100 | ix: usize, | ||
| 101 | _window: &mut Window, | ||
| 102 | _: &mut Context<Picker<Self>>, | ||
| 103 | ) { | ||
| 104 | self.selected_index = ix; | ||
| 105 | } | ||
| 106 | |||
| 107 | fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> { | ||
| 108 | self.placeholder_text.clone() | ||
| 109 | } | ||
| 110 | |||
| 111 | fn update_matches( | ||
| 112 | &mut self, | ||
| 113 | query: String, | ||
| 114 | _window: &mut Window, | ||
| 115 | cx: &mut Context<Picker<Self>>, | ||
| 116 | ) -> gpui::Task<()> { | ||
| 117 | cx.spawn(|this, mut cx| async move { | ||
| 118 | let Some(processes) = this | ||
| 119 | .update(&mut cx, |this, _| { | ||
| 120 | if let Some(processes) = this.delegate.candidates.clone() { | ||
| 121 | processes | ||
| 122 | } else { | ||
| 123 | let system = System::new_all(); | ||
| 124 | |||
| 125 | let processes = | ||
| 126 | attach_processes(&this.delegate.debug_config.kind, &system.processes()); | ||
| 127 | let candidates = processes | ||
| 128 | .into_iter() | ||
| 129 | .map(|(pid, process)| Candidate { | ||
| 130 | pid: pid.as_u32(), | ||
| 131 | name: process.name().to_string_lossy().into_owned(), | ||
| 132 | command: process | ||
| 133 | .cmd() | ||
| 134 | .iter() | ||
| 135 | .map(|s| s.to_string_lossy().to_string()) | ||
| 136 | .collect::<Vec<_>>(), | ||
| 137 | }) | ||
| 138 | .collect::<Vec<Candidate>>(); | ||
| 139 | |||
| 140 | let _ = this.delegate.candidates.insert(candidates.clone()); | ||
| 141 | |||
| 142 | candidates | ||
| 143 | } | ||
| 144 | }) | ||
| 145 | .ok() | ||
| 146 | else { | ||
| 147 | return; | ||
| 148 | }; | ||
| 149 | |||
| 150 | let matches = fuzzy::match_strings( | ||
| 151 | &processes | ||
| 152 | .iter() | ||
| 153 | .enumerate() | ||
| 154 | .map(|(id, candidate)| { | ||
| 155 | StringMatchCandidate::new( | ||
| 156 | id, | ||
| 157 | format!( | ||
| 158 | "{} {} {}", | ||
| 159 | candidate.command.join(" "), | ||
| 160 | candidate.pid, | ||
| 161 | candidate.name | ||
| 162 | ) | ||
| 163 | .as_str(), | ||
| 164 | ) | ||
| 165 | }) | ||
| 166 | .collect::<Vec<_>>(), | ||
| 167 | &query, | ||
| 168 | true, | ||
| 169 | 100, | ||
| 170 | &Default::default(), | ||
| 171 | cx.background_executor().clone(), | ||
| 172 | ) | ||
| 173 | .await; | ||
| 174 | |||
| 175 | this.update(&mut cx, |this, _| { | ||
| 176 | let delegate = &mut this.delegate; | ||
| 177 | |||
| 178 | delegate.matches = matches; | ||
| 179 | delegate.candidates = Some(processes); | ||
| 180 | |||
| 181 | if delegate.matches.is_empty() { | ||
| 182 | delegate.selected_index = 0; | ||
| 183 | } else { | ||
| 184 | delegate.selected_index = | ||
| 185 | delegate.selected_index.min(delegate.matches.len() - 1); | ||
| 186 | } | ||
| 187 | }) | ||
| 188 | .ok(); | ||
| 189 | }) | ||
| 190 | } | ||
| 191 | |||
| 192 | fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) { | ||
| 193 | let candidate = self | ||
| 194 | .matches | ||
| 195 | .get(self.selected_index()) | ||
| 196 | .and_then(|current_match| { | ||
| 197 | let ix = current_match.candidate_id; | ||
| 198 | self.candidates.as_ref().map(|candidates| &candidates[ix]) | ||
| 199 | }); | ||
| 200 | |||
| 201 | let Some(candidate) = candidate else { | ||
| 202 | return cx.emit(DismissEvent); | ||
| 203 | }; | ||
| 204 | |||
| 205 | match &mut self.debug_config.request { | ||
| 206 | DebugRequestType::Attach(config) => { | ||
| 207 | config.process_id = Some(candidate.pid); | ||
| 208 | } | ||
| 209 | DebugRequestType::Launch => { | ||
| 210 | debug_panic!("Debugger attach modal used on launch debug config"); | ||
| 211 | return; | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | let config = self.debug_config.clone(); | ||
| 216 | self.project | ||
| 217 | .update(cx, |project, cx| project.start_debug_session(config, cx)) | ||
| 218 | .detach_and_log_err(cx); | ||
| 219 | |||
| 220 | cx.emit(DismissEvent); | ||
| 221 | } | ||
| 222 | |||
| 223 | fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) { | ||
| 224 | self.selected_index = 0; | ||
| 225 | self.candidates.take(); | ||
| 226 | |||
| 227 | cx.emit(DismissEvent); | ||
| 228 | } | ||
| 229 | |||
| 230 | fn render_match( | ||
| 231 | &self, | ||
| 232 | ix: usize, | ||
| 233 | selected: bool, | ||
| 234 | _window: &mut Window, | ||
| 235 | _: &mut Context<Picker<Self>>, | ||
| 236 | ) -> Option<Self::ListItem> { | ||
| 237 | let candidates = self.candidates.as_ref()?; | ||
| 238 | let hit = &self.matches[ix]; | ||
| 239 | let candidate = &candidates.get(hit.candidate_id)?; | ||
| 240 | |||
| 241 | Some( | ||
| 242 | ListItem::new(SharedString::from(format!("process-entry-{ix}"))) | ||
| 243 | .inset(true) | ||
| 244 | .spacing(ListItemSpacing::Sparse) | ||
| 245 | .toggle_state(selected) | ||
| 246 | .child( | ||
| 247 | v_flex() | ||
| 248 | .items_start() | ||
| 249 | .child(Label::new(format!("{} {}", candidate.name, candidate.pid))) | ||
| 250 | .child( | ||
| 251 | div() | ||
| 252 | .id(SharedString::from(format!("process-entry-{ix}-command"))) | ||
| 253 | .tooltip(Tooltip::text( | ||
| 254 | candidate | ||
| 255 | .command | ||
| 256 | .clone() | ||
| 257 | .into_iter() | ||
| 258 | .collect::<Vec<_>>() | ||
| 259 | .join(" "), | ||
| 260 | )) | ||
| 261 | .child( | ||
| 262 | Label::new(format!( | ||
| 263 | "{} {}", | ||
| 264 | candidate.name, | ||
| 265 | candidate | ||
| 266 | .command | ||
| 267 | .clone() | ||
| 268 | .into_iter() | ||
| 269 | .skip(1) | ||
| 270 | .collect::<Vec<_>>() | ||
| 271 | .join(" ") | ||
| 272 | )) | ||
| 273 | .size(LabelSize::Small) | ||
| 274 | .color(Color::Muted), | ||
| 275 | ), | ||
| 276 | ), | ||
| 277 | ), | ||
| 278 | ) | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | #[allow(dead_code)] | ||
| 283 | #[cfg(any(test, feature = "test-support"))] | ||
| 284 | pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> { | ||
| 285 | modal.picker.update(cx, |picker, _| { | ||
| 286 | picker | ||
| 287 | .delegate | ||
| 288 | .matches | ||
| 289 | .iter() | ||
| 290 | .map(|hit| hit.string.clone()) | ||
| 291 | .collect::<Vec<_>>() | ||
| 292 | }) | ||
| 293 | } | ||
| @@ -0,0 +1,536 @@ | |||
| 1 | use crate::session::DebugSession; | ||
| 2 | use anyhow::{anyhow, Result}; | ||
| 3 | use collections::HashMap; | ||
| 4 | use command_palette_hooks::CommandPaletteFilter; | ||
| 5 | use dap::{ | ||
| 6 | client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent, | ||
| 7 | ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, | ||
| 8 | }; | ||
| 9 | use futures::{channel::mpsc, SinkExt as _}; | ||
| 10 | use gpui::{ | ||
| 11 | actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, | ||
| 12 | Focusable, Subscription, Task, WeakEntity, | ||
| 13 | }; | ||
| 14 | use project::{ | ||
| 15 | debugger::dap_store::{self, DapStore}, | ||
| 16 | terminals::TerminalKind, | ||
| 17 | Project, | ||
| 18 | }; | ||
| 19 | use rpc::proto::{self}; | ||
| 20 | use settings::Settings; | ||
| 21 | use std::{any::TypeId, path::PathBuf}; | ||
| 22 | use terminal_view::terminal_panel::TerminalPanel; | ||
| 23 | use ui::prelude::*; | ||
| 24 | use workspace::{ | ||
| 25 | dock::{DockPosition, Panel, PanelEvent}, | ||
| 26 | pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop, | ||
| 27 | ToggleIgnoreBreakpoints, Workspace, | ||
| 28 | }; | ||
| 29 | |||
| 30 | pub enum DebugPanelEvent { | ||
| 31 | Exited(SessionId), | ||
| 32 | Terminated(SessionId), | ||
| 33 | Stopped { | ||
| 34 | client_id: SessionId, | ||
| 35 | event: StoppedEvent, | ||
| 36 | go_to_stack_frame: bool, | ||
| 37 | }, | ||
| 38 | Thread((SessionId, ThreadEvent)), | ||
| 39 | Continued((SessionId, ContinuedEvent)), | ||
| 40 | Output((SessionId, OutputEvent)), | ||
| 41 | Module((SessionId, ModuleEvent)), | ||
| 42 | LoadedSource((SessionId, LoadedSourceEvent)), | ||
| 43 | ClientShutdown(SessionId), | ||
| 44 | CapabilitiesChanged(SessionId), | ||
| 45 | } | ||
| 46 | |||
| 47 | actions!(debug_panel, [ToggleFocus]); | ||
| 48 | pub struct DebugPanel { | ||
| 49 | size: Pixels, | ||
| 50 | pane: Entity<Pane>, | ||
| 51 | project: WeakEntity<Project>, | ||
| 52 | workspace: WeakEntity<Workspace>, | ||
| 53 | _subscriptions: Vec<Subscription>, | ||
| 54 | } | ||
| 55 | |||
| 56 | impl DebugPanel { | ||
| 57 | pub fn new( | ||
| 58 | workspace: &Workspace, | ||
| 59 | window: &mut Window, | ||
| 60 | cx: &mut Context<Workspace>, | ||
| 61 | ) -> Entity<Self> { | ||
| 62 | cx.new(|cx| { | ||
| 63 | let project = workspace.project().clone(); | ||
| 64 | let dap_store = project.read(cx).dap_store(); | ||
| 65 | let weak_workspace = workspace.weak_handle(); | ||
| 66 | let pane = cx.new(|cx| { | ||
| 67 | let mut pane = Pane::new( | ||
| 68 | workspace.weak_handle(), | ||
| 69 | project.clone(), | ||
| 70 | Default::default(), | ||
| 71 | None, | ||
| 72 | gpui::NoAction.boxed_clone(), | ||
| 73 | window, | ||
| 74 | cx, | ||
| 75 | ); | ||
| 76 | pane.set_can_split(None); | ||
| 77 | pane.set_can_navigate(true, cx); | ||
| 78 | pane.display_nav_history_buttons(None); | ||
| 79 | pane.set_should_display_tab_bar(|_window, _cx| true); | ||
| 80 | pane.set_close_pane_if_empty(true, cx); | ||
| 81 | pane.set_render_tab_bar_buttons(cx, { | ||
| 82 | let project = project.clone(); | ||
| 83 | let weak_workspace = weak_workspace.clone(); | ||
| 84 | move |_, _, cx| { | ||
| 85 | let project = project.clone(); | ||
| 86 | let weak_workspace = weak_workspace.clone(); | ||
| 87 | ( | ||
| 88 | None, | ||
| 89 | Some( | ||
| 90 | h_flex() | ||
| 91 | .child( | ||
| 92 | IconButton::new("new-debug-session", IconName::Plus) | ||
| 93 | .icon_size(IconSize::Small) | ||
| 94 | .on_click(cx.listener(move |pane, _, window, cx| { | ||
| 95 | pane.add_item( | ||
| 96 | Box::new(DebugSession::inert( | ||
| 97 | project.clone(), | ||
| 98 | weak_workspace.clone(), | ||
| 99 | window, | ||
| 100 | cx, | ||
| 101 | )), | ||
| 102 | false, | ||
| 103 | false, | ||
| 104 | None, | ||
| 105 | window, | ||
| 106 | cx, | ||
| 107 | ); | ||
| 108 | })), | ||
| 109 | ) | ||
| 110 | .into_any_element(), | ||
| 111 | ), | ||
| 112 | ) | ||
| 113 | } | ||
| 114 | }); | ||
| 115 | pane.add_item( | ||
| 116 | Box::new(DebugSession::inert( | ||
| 117 | project.clone(), | ||
| 118 | weak_workspace.clone(), | ||
| 119 | window, | ||
| 120 | cx, | ||
| 121 | )), | ||
| 122 | false, | ||
| 123 | false, | ||
| 124 | None, | ||
| 125 | window, | ||
| 126 | cx, | ||
| 127 | ); | ||
| 128 | pane | ||
| 129 | }); | ||
| 130 | |||
| 131 | let _subscriptions = vec![ | ||
| 132 | cx.observe(&pane, |_, _, cx| cx.notify()), | ||
| 133 | cx.subscribe_in(&pane, window, Self::handle_pane_event), | ||
| 134 | cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event), | ||
| 135 | ]; | ||
| 136 | |||
| 137 | let debug_panel = Self { | ||
| 138 | pane, | ||
| 139 | size: px(300.), | ||
| 140 | _subscriptions, | ||
| 141 | project: project.downgrade(), | ||
| 142 | workspace: workspace.weak_handle(), | ||
| 143 | }; | ||
| 144 | |||
| 145 | debug_panel | ||
| 146 | }) | ||
| 147 | } | ||
| 148 | |||
| 149 | pub fn load( | ||
| 150 | workspace: WeakEntity<Workspace>, | ||
| 151 | cx: AsyncWindowContext, | ||
| 152 | ) -> Task<Result<Entity<Self>>> { | ||
| 153 | cx.spawn(|mut cx| async move { | ||
| 154 | workspace.update_in(&mut cx, |workspace, window, cx| { | ||
| 155 | let debug_panel = DebugPanel::new(workspace, window, cx); | ||
| 156 | |||
| 157 | cx.observe(&debug_panel, |_, debug_panel, cx| { | ||
| 158 | let (has_active_session, supports_restart, support_step_back) = debug_panel | ||
| 159 | .update(cx, |this, cx| { | ||
| 160 | this.active_session(cx) | ||
| 161 | .map(|item| { | ||
| 162 | let running = item.read(cx).mode().as_running().cloned(); | ||
| 163 | |||
| 164 | match running { | ||
| 165 | Some(running) => { | ||
| 166 | let caps = running.read(cx).capabilities(cx); | ||
| 167 | ( | ||
| 168 | true, | ||
| 169 | caps.supports_restart_request.unwrap_or_default(), | ||
| 170 | caps.supports_step_back.unwrap_or_default(), | ||
| 171 | ) | ||
| 172 | } | ||
| 173 | None => (false, false, false), | ||
| 174 | } | ||
| 175 | }) | ||
| 176 | .unwrap_or((false, false, false)) | ||
| 177 | }); | ||
| 178 | |||
| 179 | let filter = CommandPaletteFilter::global_mut(cx); | ||
| 180 | let debugger_action_types = [ | ||
| 181 | TypeId::of::<Continue>(), | ||
| 182 | TypeId::of::<StepOver>(), | ||
| 183 | TypeId::of::<StepInto>(), | ||
| 184 | TypeId::of::<StepOut>(), | ||
| 185 | TypeId::of::<Stop>(), | ||
| 186 | TypeId::of::<Disconnect>(), | ||
| 187 | TypeId::of::<Pause>(), | ||
| 188 | TypeId::of::<ToggleIgnoreBreakpoints>(), | ||
| 189 | ]; | ||
| 190 | |||
| 191 | let step_back_action_type = [TypeId::of::<StepBack>()]; | ||
| 192 | let restart_action_type = [TypeId::of::<Restart>()]; | ||
| 193 | |||
| 194 | if has_active_session { | ||
| 195 | filter.show_action_types(debugger_action_types.iter()); | ||
| 196 | |||
| 197 | if supports_restart { | ||
| 198 | filter.show_action_types(restart_action_type.iter()); | ||
| 199 | } else { | ||
| 200 | filter.hide_action_types(&restart_action_type); | ||
| 201 | } | ||
| 202 | |||
| 203 | if support_step_back { | ||
| 204 | filter.show_action_types(step_back_action_type.iter()); | ||
| 205 | } else { | ||
| 206 | filter.hide_action_types(&step_back_action_type); | ||
| 207 | } | ||
| 208 | } else { | ||
| 209 | // show only the `debug: start` | ||
| 210 | filter.hide_action_types(&debugger_action_types); | ||
| 211 | filter.hide_action_types(&step_back_action_type); | ||
| 212 | filter.hide_action_types(&restart_action_type); | ||
| 213 | } | ||
| 214 | }) | ||
| 215 | .detach(); | ||
| 216 | |||
| 217 | debug_panel | ||
| 218 | }) | ||
| 219 | }) | ||
| 220 | } | ||
| 221 | |||
| 222 | pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> { | ||
| 223 | self.pane | ||
| 224 | .read(cx) | ||
| 225 | .active_item() | ||
| 226 | .and_then(|panel| panel.downcast::<DebugSession>()) | ||
| 227 | } | ||
| 228 | |||
| 229 | pub fn debug_panel_items_by_client( | ||
| 230 | &self, | ||
| 231 | client_id: &SessionId, | ||
| 232 | cx: &Context<Self>, | ||
| 233 | ) -> Vec<Entity<DebugSession>> { | ||
| 234 | self.pane | ||
| 235 | .read(cx) | ||
| 236 | .items() | ||
| 237 | .filter_map(|item| item.downcast::<DebugSession>()) | ||
| 238 | .filter(|item| item.read(cx).session_id(cx) == Some(*client_id)) | ||
| 239 | .map(|item| item.clone()) | ||
| 240 | .collect() | ||
| 241 | } | ||
| 242 | |||
| 243 | pub fn debug_panel_item_by_client( | ||
| 244 | &self, | ||
| 245 | client_id: SessionId, | ||
| 246 | cx: &mut Context<Self>, | ||
| 247 | ) -> Option<Entity<DebugSession>> { | ||
| 248 | self.pane | ||
| 249 | .read(cx) | ||
| 250 | .items() | ||
| 251 | .filter_map(|item| item.downcast::<DebugSession>()) | ||
| 252 | .find(|item| { | ||
| 253 | let item = item.read(cx); | ||
| 254 | |||
| 255 | item.session_id(cx) == Some(client_id) | ||
| 256 | }) | ||
| 257 | } | ||
| 258 | |||
| 259 | fn handle_dap_store_event( | ||
| 260 | &mut self, | ||
| 261 | dap_store: &Entity<DapStore>, | ||
| 262 | event: &dap_store::DapStoreEvent, | ||
| 263 | window: &mut Window, | ||
| 264 | cx: &mut Context<Self>, | ||
| 265 | ) { | ||
| 266 | match event { | ||
| 267 | dap_store::DapStoreEvent::DebugClientStarted(session_id) => { | ||
| 268 | let Some(session) = dap_store.read(cx).session_by_id(session_id) else { | ||
| 269 | return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event"); | ||
| 270 | }; | ||
| 271 | |||
| 272 | let Some(project) = self.project.upgrade() else { | ||
| 273 | return log::error!("Debug Panel out lived it's weak reference to Project"); | ||
| 274 | }; | ||
| 275 | |||
| 276 | if self.pane.read_with(cx, |pane, cx| { | ||
| 277 | pane.items_of_type::<DebugSession>() | ||
| 278 | .any(|item| item.read(cx).session_id(cx) == Some(*session_id)) | ||
| 279 | }) { | ||
| 280 | // We already have an item for this session. | ||
| 281 | return; | ||
| 282 | } | ||
| 283 | let session_item = | ||
| 284 | DebugSession::running(project, self.workspace.clone(), session, window, cx); | ||
| 285 | |||
| 286 | self.pane.update(cx, |pane, cx| { | ||
| 287 | pane.add_item(Box::new(session_item), true, true, None, window, cx); | ||
| 288 | window.focus(&pane.focus_handle(cx)); | ||
| 289 | cx.notify(); | ||
| 290 | }); | ||
| 291 | } | ||
| 292 | dap_store::DapStoreEvent::RunInTerminal { | ||
| 293 | title, | ||
| 294 | cwd, | ||
| 295 | command, | ||
| 296 | args, | ||
| 297 | envs, | ||
| 298 | sender, | ||
| 299 | .. | ||
| 300 | } => { | ||
| 301 | self.handle_run_in_terminal_request( | ||
| 302 | title.clone(), | ||
| 303 | cwd.clone(), | ||
| 304 | command.clone(), | ||
| 305 | args.clone(), | ||
| 306 | envs.clone(), | ||
| 307 | sender.clone(), | ||
| 308 | window, | ||
| 309 | cx, | ||
| 310 | ) | ||
| 311 | .detach_and_log_err(cx); | ||
| 312 | } | ||
| 313 | _ => {} | ||
| 314 | } | ||
| 315 | } | ||
| 316 | |||
| 317 | fn handle_run_in_terminal_request( | ||
| 318 | &self, | ||
| 319 | title: Option<String>, | ||
| 320 | cwd: PathBuf, | ||
| 321 | command: Option<String>, | ||
| 322 | args: Vec<String>, | ||
| 323 | envs: HashMap<String, String>, | ||
| 324 | mut sender: mpsc::Sender<Result<u32>>, | ||
| 325 | window: &mut Window, | ||
| 326 | cx: &mut App, | ||
| 327 | ) -> Task<Result<()>> { | ||
| 328 | let terminal_task = self.workspace.update(cx, |workspace, cx| { | ||
| 329 | let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| { | ||
| 330 | anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found") | ||
| 331 | }); | ||
| 332 | |||
| 333 | let terminal_panel = match terminal_panel { | ||
| 334 | Ok(panel) => panel, | ||
| 335 | Err(err) => return Task::ready(Err(err)), | ||
| 336 | }; | ||
| 337 | |||
| 338 | terminal_panel.update(cx, |terminal_panel, cx| { | ||
| 339 | let terminal_task = terminal_panel.add_terminal( | ||
| 340 | TerminalKind::Debug { | ||
| 341 | command, | ||
| 342 | args, | ||
| 343 | envs, | ||
| 344 | cwd, | ||
| 345 | title, | ||
| 346 | }, | ||
| 347 | task::RevealStrategy::Always, | ||
| 348 | window, | ||
| 349 | cx, | ||
| 350 | ); | ||
| 351 | |||
| 352 | cx.spawn(|_, mut cx| async move { | ||
| 353 | let pid_task = async move { | ||
| 354 | let terminal = terminal_task.await?; | ||
| 355 | |||
| 356 | terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid()) | ||
| 357 | }; | ||
| 358 | |||
| 359 | pid_task.await | ||
| 360 | }) | ||
| 361 | }) | ||
| 362 | }); | ||
| 363 | |||
| 364 | cx.background_spawn(async move { | ||
| 365 | match terminal_task { | ||
| 366 | Ok(pid_task) => match pid_task.await { | ||
| 367 | Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?, | ||
| 368 | Ok(None) => { | ||
| 369 | sender | ||
| 370 | .send(Err(anyhow!( | ||
| 371 | "Terminal was spawned but PID was not available" | ||
| 372 | ))) | ||
| 373 | .await? | ||
| 374 | } | ||
| 375 | Err(error) => sender.send(Err(anyhow!(error))).await?, | ||
| 376 | }, | ||
| 377 | Err(error) => sender.send(Err(anyhow!(error))).await?, | ||
| 378 | }; | ||
| 379 | |||
| 380 | Ok(()) | ||
| 381 | }) | ||
| 382 | } | ||
| 383 | |||
| 384 | fn handle_pane_event( | ||
| 385 | &mut self, | ||
| 386 | _: &Entity<Pane>, | ||
| 387 | event: &pane::Event, | ||
| 388 | window: &mut Window, | ||
| 389 | cx: &mut Context<Self>, | ||
| 390 | ) { | ||
| 391 | match event { | ||
| 392 | pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), | ||
| 393 | pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), | ||
| 394 | pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), | ||
| 395 | pane::Event::AddItem { item } => { | ||
| 396 | self.workspace | ||
| 397 | .update(cx, |workspace, cx| { | ||
| 398 | item.added_to_pane(workspace, self.pane.clone(), window, cx) | ||
| 399 | }) | ||
| 400 | .ok(); | ||
| 401 | } | ||
| 402 | pane::Event::RemovedItem { item } => { | ||
| 403 | if let Some(debug_session) = item.downcast::<DebugSession>() { | ||
| 404 | debug_session.update(cx, |session, cx| { | ||
| 405 | session.shutdown(cx); | ||
| 406 | }) | ||
| 407 | } | ||
| 408 | } | ||
| 409 | pane::Event::ActivateItem { | ||
| 410 | local: _, | ||
| 411 | focus_changed, | ||
| 412 | } => { | ||
| 413 | if *focus_changed { | ||
| 414 | if let Some(debug_session) = self | ||
| 415 | .pane | ||
| 416 | .read(cx) | ||
| 417 | .active_item() | ||
| 418 | .and_then(|item| item.downcast::<DebugSession>()) | ||
| 419 | { | ||
| 420 | if let Some(running) = debug_session | ||
| 421 | .read_with(cx, |session, _| session.mode().as_running().cloned()) | ||
| 422 | { | ||
| 423 | running.update(cx, |running, cx| { | ||
| 424 | running.go_to_selected_stack_frame(window, cx); | ||
| 425 | }); | ||
| 426 | } | ||
| 427 | } | ||
| 428 | } | ||
| 429 | } | ||
| 430 | |||
| 431 | _ => {} | ||
| 432 | } | ||
| 433 | } | ||
| 434 | } | ||
| 435 | |||
| 436 | impl EventEmitter<PanelEvent> for DebugPanel {} | ||
| 437 | impl EventEmitter<DebugPanelEvent> for DebugPanel {} | ||
| 438 | impl EventEmitter<project::Event> for DebugPanel {} | ||
| 439 | |||
| 440 | impl Focusable for DebugPanel { | ||
| 441 | fn focus_handle(&self, cx: &App) -> FocusHandle { | ||
| 442 | self.pane.focus_handle(cx) | ||
| 443 | } | ||
| 444 | } | ||
| 445 | |||
| 446 | impl Panel for DebugPanel { | ||
| 447 | fn pane(&self) -> Option<Entity<Pane>> { | ||
| 448 | Some(self.pane.clone()) | ||
| 449 | } | ||
| 450 | |||
| 451 | fn persistent_name() -> &'static str { | ||
| 452 | "DebugPanel" | ||
| 453 | } | ||
| 454 | |||
| 455 | fn position(&self, _window: &Window, _cx: &App) -> DockPosition { | ||
| 456 | DockPosition::Bottom | ||
| 457 | } | ||
| 458 | |||
| 459 | fn position_is_valid(&self, position: DockPosition) -> bool { | ||
| 460 | position == DockPosition::Bottom | ||
| 461 | } | ||
| 462 | |||
| 463 | fn set_position( | ||
| 464 | &mut self, | ||
| 465 | _position: DockPosition, | ||
| 466 | _window: &mut Window, | ||
| 467 | _cx: &mut Context<Self>, | ||
| 468 | ) { | ||
| 469 | } | ||
| 470 | |||
| 471 | fn size(&self, _window: &Window, _cx: &App) -> Pixels { | ||
| 472 | self.size | ||
| 473 | } | ||
| 474 | |||
| 475 | fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) { | ||
| 476 | self.size = size.unwrap(); | ||
| 477 | } | ||
| 478 | |||
| 479 | fn remote_id() -> Option<proto::PanelId> { | ||
| 480 | Some(proto::PanelId::DebugPanel) | ||
| 481 | } | ||
| 482 | |||
| 483 | fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> { | ||
| 484 | Some(IconName::Debug) | ||
| 485 | } | ||
| 486 | |||
| 487 | fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> { | ||
| 488 | if DebuggerSettings::get_global(cx).button { | ||
| 489 | Some("Debug Panel") | ||
| 490 | } else { | ||
| 491 | None | ||
| 492 | } | ||
| 493 | } | ||
| 494 | |||
| 495 | fn toggle_action(&self) -> Box<dyn Action> { | ||
| 496 | Box::new(ToggleFocus) | ||
| 497 | } | ||
| 498 | |||
| 499 | fn activation_priority(&self) -> u32 { | ||
| 500 | 9 | ||
| 501 | } | ||
| 502 | fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) { | ||
| 503 | if active && self.pane.read(cx).items_len() == 0 { | ||
| 504 | let Some(project) = self.project.clone().upgrade() else { | ||
| 505 | return; | ||
| 506 | }; | ||
| 507 | // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items). | ||
| 508 | self.pane.update(cx, |this, cx| { | ||
| 509 | this.add_item( | ||
| 510 | Box::new(DebugSession::inert( | ||
| 511 | project, | ||
| 512 | self.workspace.clone(), | ||
| 513 | window, | ||
| 514 | cx, | ||
| 515 | )), | ||
| 516 | false, | ||
| 517 | false, | ||
| 518 | None, | ||
| 519 | window, | ||
| 520 | cx, | ||
| 521 | ); | ||
| 522 | }); | ||
| 523 | } | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | impl Render for DebugPanel { | ||
| 528 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 529 | v_flex() | ||
| 530 | .key_context("DebugPanel") | ||
| 531 | .track_focus(&self.focus_handle(cx)) | ||
| 532 | .size_full() | ||
| 533 | .child(self.pane.clone()) | ||
| 534 | .into_any() | ||
| 535 | } | ||
| 536 | } | ||
| @@ -0,0 +1,122 @@ | |||
| 1 | use dap::debugger_settings::DebuggerSettings; | ||
| 2 | use debugger_panel::{DebugPanel, ToggleFocus}; | ||
| 3 | use feature_flags::{Debugger, FeatureFlagViewExt}; | ||
| 4 | use gpui::App; | ||
| 5 | use session::DebugSession; | ||
| 6 | use settings::Settings; | ||
| 7 | use workspace::{ | ||
| 8 | Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop, | ||
| 9 | ToggleIgnoreBreakpoints, Workspace, | ||
| 10 | }; | ||
| 11 | |||
| 12 | pub mod attach_modal; | ||
| 13 | pub mod debugger_panel; | ||
| 14 | pub mod session; | ||
| 15 | |||
| 16 | #[cfg(test)] | ||
| 17 | mod tests; | ||
| 18 | |||
| 19 | pub fn init(cx: &mut App) { | ||
| 20 | DebuggerSettings::register(cx); | ||
| 21 | workspace::FollowableViewRegistry::register::<DebugSession>(cx); | ||
| 22 | |||
| 23 | cx.observe_new(|_: &mut Workspace, window, cx| { | ||
| 24 | let Some(window) = window else { | ||
| 25 | return; | ||
| 26 | }; | ||
| 27 | |||
| 28 | cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| { | ||
| 29 | workspace | ||
| 30 | .register_action(|workspace, _: &ToggleFocus, window, cx| { | ||
| 31 | workspace.toggle_panel_focus::<DebugPanel>(window, cx); | ||
| 32 | }) | ||
| 33 | .register_action(|workspace, _: &Pause, _, cx| { | ||
| 34 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 35 | |||
| 36 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 37 | panel | ||
| 38 | .active_session(cx) | ||
| 39 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 40 | }) { | ||
| 41 | active_item.update(cx, |item, cx| item.pause_thread(cx)) | ||
| 42 | } | ||
| 43 | }) | ||
| 44 | .register_action(|workspace, _: &Restart, _, cx| { | ||
| 45 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 46 | |||
| 47 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 48 | panel | ||
| 49 | .active_session(cx) | ||
| 50 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 51 | }) { | ||
| 52 | active_item.update(cx, |item, cx| item.restart_session(cx)) | ||
| 53 | } | ||
| 54 | }) | ||
| 55 | .register_action(|workspace, _: &StepInto, _, cx| { | ||
| 56 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 57 | |||
| 58 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 59 | panel | ||
| 60 | .active_session(cx) | ||
| 61 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 62 | }) { | ||
| 63 | active_item.update(cx, |item, cx| item.step_in(cx)) | ||
| 64 | } | ||
| 65 | }) | ||
| 66 | .register_action(|workspace, _: &StepOver, _, cx| { | ||
| 67 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 68 | |||
| 69 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 70 | panel | ||
| 71 | .active_session(cx) | ||
| 72 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 73 | }) { | ||
| 74 | active_item.update(cx, |item, cx| item.step_over(cx)) | ||
| 75 | } | ||
| 76 | }) | ||
| 77 | .register_action(|workspace, _: &StepBack, _, cx| { | ||
| 78 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 79 | |||
| 80 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 81 | panel | ||
| 82 | .active_session(cx) | ||
| 83 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 84 | }) { | ||
| 85 | active_item.update(cx, |item, cx| item.step_back(cx)) | ||
| 86 | } | ||
| 87 | }) | ||
| 88 | .register_action(|workspace, _: &Stop, _, cx| { | ||
| 89 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 90 | |||
| 91 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 92 | panel | ||
| 93 | .active_session(cx) | ||
| 94 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 95 | }) { | ||
| 96 | active_item.update(cx, |item, cx| item.stop_thread(cx)) | ||
| 97 | } | ||
| 98 | }) | ||
| 99 | .register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| { | ||
| 100 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 101 | |||
| 102 | if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { | ||
| 103 | panel | ||
| 104 | .active_session(cx) | ||
| 105 | .and_then(|session| session.read(cx).mode().as_running().cloned()) | ||
| 106 | }) { | ||
| 107 | active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) | ||
| 108 | } | ||
| 109 | }) | ||
| 110 | .register_action( | ||
| 111 | |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { | ||
| 112 | workspace.project().update(cx, |project, cx| { | ||
| 113 | project.dap_store().update(cx, |store, cx| { | ||
| 114 | store.shutdown_sessions(cx).detach(); | ||
| 115 | }) | ||
| 116 | }) | ||
| 117 | }, | ||
| 118 | ); | ||
| 119 | }) | ||
| 120 | }) | ||
| 121 | .detach(); | ||
| 122 | } | ||
| @@ -0,0 +1,313 @@ | |||
| 1 | mod failed; | ||
| 2 | mod inert; | ||
| 3 | pub mod running; | ||
| 4 | mod starting; | ||
| 5 | |||
| 6 | use std::time::Duration; | ||
| 7 | |||
| 8 | use dap::client::SessionId; | ||
| 9 | use failed::FailedState; | ||
| 10 | use gpui::{ | ||
| 11 | percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle, | ||
| 12 | Focusable, Subscription, Task, Transformation, WeakEntity, | ||
| 13 | }; | ||
| 14 | use inert::{InertEvent, InertState}; | ||
| 15 | use project::debugger::{dap_store::DapStore, session::Session}; | ||
| 16 | use project::worktree_store::WorktreeStore; | ||
| 17 | use project::Project; | ||
| 18 | use rpc::proto::{self, PeerId}; | ||
| 19 | use running::RunningState; | ||
| 20 | use starting::{StartingEvent, StartingState}; | ||
| 21 | use ui::prelude::*; | ||
| 22 | use workspace::{ | ||
| 23 | item::{self, Item}, | ||
| 24 | FollowableItem, ViewId, Workspace, | ||
| 25 | }; | ||
| 26 | |||
| 27 | pub(crate) enum DebugSessionState { | ||
| 28 | Inert(Entity<InertState>), | ||
| 29 | Starting(Entity<StartingState>), | ||
| 30 | Failed(Entity<FailedState>), | ||
| 31 | Running(Entity<running::RunningState>), | ||
| 32 | } | ||
| 33 | |||
| 34 | impl DebugSessionState { | ||
| 35 | pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> { | ||
| 36 | match &self { | ||
| 37 | DebugSessionState::Running(entity) => Some(entity), | ||
| 38 | _ => None, | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | pub struct DebugSession { | ||
| 44 | remote_id: Option<workspace::ViewId>, | ||
| 45 | mode: DebugSessionState, | ||
| 46 | dap_store: WeakEntity<DapStore>, | ||
| 47 | worktree_store: WeakEntity<WorktreeStore>, | ||
| 48 | workspace: WeakEntity<Workspace>, | ||
| 49 | _subscriptions: [Subscription; 1], | ||
| 50 | } | ||
| 51 | |||
| 52 | #[derive(Debug)] | ||
| 53 | pub enum DebugPanelItemEvent { | ||
| 54 | Close, | ||
| 55 | Stopped { go_to_stack_frame: bool }, | ||
| 56 | } | ||
| 57 | |||
| 58 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] | ||
| 59 | pub enum ThreadItem { | ||
| 60 | Console, | ||
| 61 | LoadedSource, | ||
| 62 | Modules, | ||
| 63 | Variables, | ||
| 64 | } | ||
| 65 | |||
| 66 | impl DebugSession { | ||
| 67 | pub(super) fn inert( | ||
| 68 | project: Entity<Project>, | ||
| 69 | workspace: WeakEntity<Workspace>, | ||
| 70 | window: &mut Window, | ||
| 71 | cx: &mut App, | ||
| 72 | ) -> Entity<Self> { | ||
| 73 | let default_cwd = project | ||
| 74 | .read(cx) | ||
| 75 | .worktrees(cx) | ||
| 76 | .next() | ||
| 77 | .and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string())) | ||
| 78 | .unwrap_or_default(); | ||
| 79 | |||
| 80 | let inert = cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, window, cx)); | ||
| 81 | |||
| 82 | let project = project.read(cx); | ||
| 83 | let dap_store = project.dap_store().downgrade(); | ||
| 84 | let worktree_store = project.worktree_store().downgrade(); | ||
| 85 | cx.new(|cx| { | ||
| 86 | let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)]; | ||
| 87 | Self { | ||
| 88 | remote_id: None, | ||
| 89 | mode: DebugSessionState::Inert(inert), | ||
| 90 | dap_store, | ||
| 91 | worktree_store, | ||
| 92 | workspace, | ||
| 93 | _subscriptions, | ||
| 94 | } | ||
| 95 | }) | ||
| 96 | } | ||
| 97 | |||
| 98 | pub(crate) fn running( | ||
| 99 | project: Entity<Project>, | ||
| 100 | workspace: WeakEntity<Workspace>, | ||
| 101 | session: Entity<Session>, | ||
| 102 | window: &mut Window, | ||
| 103 | cx: &mut App, | ||
| 104 | ) -> Entity<Self> { | ||
| 105 | let mode = cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx)); | ||
| 106 | |||
| 107 | cx.new(|cx| Self { | ||
| 108 | _subscriptions: [cx.subscribe(&mode, |_, _, _, cx| { | ||
| 109 | cx.notify(); | ||
| 110 | })], | ||
| 111 | remote_id: None, | ||
| 112 | mode: DebugSessionState::Running(mode), | ||
| 113 | dap_store: project.read(cx).dap_store().downgrade(), | ||
| 114 | worktree_store: project.read(cx).worktree_store().downgrade(), | ||
| 115 | workspace, | ||
| 116 | }) | ||
| 117 | } | ||
| 118 | |||
| 119 | pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> { | ||
| 120 | match &self.mode { | ||
| 121 | DebugSessionState::Inert(_) => None, | ||
| 122 | DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id), | ||
| 123 | DebugSessionState::Failed(_) => None, | ||
| 124 | DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()), | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) { | ||
| 129 | match &self.mode { | ||
| 130 | DebugSessionState::Inert(_) => {} | ||
| 131 | DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit) | ||
| 132 | DebugSessionState::Failed(_) => {} | ||
| 133 | DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)), | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | pub(crate) fn mode(&self) -> &DebugSessionState { | ||
| 138 | &self.mode | ||
| 139 | } | ||
| 140 | |||
| 141 | fn on_inert_event( | ||
| 142 | &mut self, | ||
| 143 | _: &Entity<InertState>, | ||
| 144 | event: &InertEvent, | ||
| 145 | window: &mut Window, | ||
| 146 | cx: &mut Context<'_, Self>, | ||
| 147 | ) { | ||
| 148 | let dap_store = self.dap_store.clone(); | ||
| 149 | let InertEvent::Spawned { config } = event; | ||
| 150 | let config = config.clone(); | ||
| 151 | let worktree = self | ||
| 152 | .worktree_store | ||
| 153 | .update(cx, |this, _| this.worktrees().next()) | ||
| 154 | .ok() | ||
| 155 | .flatten() | ||
| 156 | .expect("worktree-less project"); | ||
| 157 | let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| { | ||
| 158 | store.new_session(config, &worktree, None, cx) | ||
| 159 | }) else { | ||
| 160 | return; | ||
| 161 | }; | ||
| 162 | let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx)); | ||
| 163 | |||
| 164 | self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)]; | ||
| 165 | self.mode = DebugSessionState::Starting(starting); | ||
| 166 | } | ||
| 167 | |||
| 168 | fn on_starting_event( | ||
| 169 | &mut self, | ||
| 170 | _: &Entity<StartingState>, | ||
| 171 | event: &StartingEvent, | ||
| 172 | window: &mut Window, | ||
| 173 | cx: &mut Context<'_, Self>, | ||
| 174 | ) { | ||
| 175 | if let StartingEvent::Finished(session) = event { | ||
| 176 | let mode = | ||
| 177 | cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx)); | ||
| 178 | self.mode = DebugSessionState::Running(mode); | ||
| 179 | } else if let StartingEvent::Failed = event { | ||
| 180 | self.mode = DebugSessionState::Failed(cx.new(FailedState::new)); | ||
| 181 | }; | ||
| 182 | cx.notify(); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | impl EventEmitter<DebugPanelItemEvent> for DebugSession {} | ||
| 186 | |||
| 187 | impl Focusable for DebugSession { | ||
| 188 | fn focus_handle(&self, cx: &App) -> FocusHandle { | ||
| 189 | match &self.mode { | ||
| 190 | DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx), | ||
| 191 | DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx), | ||
| 192 | DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx), | ||
| 193 | DebugSessionState::Running(running_state) => running_state.focus_handle(cx), | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | impl Item for DebugSession { | ||
| 199 | type Event = DebugPanelItemEvent; | ||
| 200 | fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement { | ||
| 201 | let (label, color) = match &self.mode { | ||
| 202 | DebugSessionState::Inert(_) => ("New Session", Color::Default), | ||
| 203 | DebugSessionState::Starting(_) => ("Starting", Color::Default), | ||
| 204 | DebugSessionState::Failed(_) => ("Failed", Color::Error), | ||
| 205 | DebugSessionState::Running(state) => ( | ||
| 206 | state | ||
| 207 | .read_with(cx, |state, cx| state.thread_status(cx)) | ||
| 208 | .map(|status| status.label()) | ||
| 209 | .unwrap_or("Running"), | ||
| 210 | Color::Default, | ||
| 211 | ), | ||
| 212 | }; | ||
| 213 | |||
| 214 | let is_starting = matches!(self.mode, DebugSessionState::Starting(_)); | ||
| 215 | |||
| 216 | h_flex() | ||
| 217 | .gap_1() | ||
| 218 | .children(is_starting.then(|| { | ||
| 219 | Icon::new(IconName::ArrowCircle).with_animation( | ||
| 220 | "starting-debug-session", | ||
| 221 | Animation::new(Duration::from_secs(2)).repeat(), | ||
| 222 | |this, delta| this.transform(Transformation::rotate(percentage(delta))), | ||
| 223 | ) | ||
| 224 | })) | ||
| 225 | .child(Label::new(label).color(color)) | ||
| 226 | .into_any_element() | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | impl FollowableItem for DebugSession { | ||
| 231 | fn remote_id(&self) -> Option<workspace::ViewId> { | ||
| 232 | self.remote_id | ||
| 233 | } | ||
| 234 | |||
| 235 | fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option<proto::view::Variant> { | ||
| 236 | None | ||
| 237 | } | ||
| 238 | |||
| 239 | fn from_state_proto( | ||
| 240 | _workspace: Entity<Workspace>, | ||
| 241 | _remote_id: ViewId, | ||
| 242 | _state: &mut Option<proto::view::Variant>, | ||
| 243 | _window: &mut Window, | ||
| 244 | _cx: &mut App, | ||
| 245 | ) -> Option<gpui::Task<gpui::Result<Entity<Self>>>> { | ||
| 246 | None | ||
| 247 | } | ||
| 248 | |||
| 249 | fn add_event_to_update_proto( | ||
| 250 | &self, | ||
| 251 | _event: &Self::Event, | ||
| 252 | _update: &mut Option<proto::update_view::Variant>, | ||
| 253 | _window: &Window, | ||
| 254 | _cx: &App, | ||
| 255 | ) -> bool { | ||
| 256 | // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default())); | ||
| 257 | |||
| 258 | true | ||
| 259 | } | ||
| 260 | |||
| 261 | fn apply_update_proto( | ||
| 262 | &mut self, | ||
| 263 | _project: &Entity<project::Project>, | ||
| 264 | _message: proto::update_view::Variant, | ||
| 265 | _window: &mut Window, | ||
| 266 | _cx: &mut Context<Self>, | ||
| 267 | ) -> gpui::Task<gpui::Result<()>> { | ||
| 268 | Task::ready(Ok(())) | ||
| 269 | } | ||
| 270 | |||
| 271 | fn set_leader_peer_id( | ||
| 272 | &mut self, | ||
| 273 | _leader_peer_id: Option<PeerId>, | ||
| 274 | _window: &mut Window, | ||
| 275 | _cx: &mut Context<Self>, | ||
| 276 | ) { | ||
| 277 | } | ||
| 278 | |||
| 279 | fn to_follow_event(_event: &Self::Event) -> Option<workspace::item::FollowEvent> { | ||
| 280 | None | ||
| 281 | } | ||
| 282 | |||
| 283 | fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<workspace::item::Dedup> { | ||
| 284 | if existing.session_id(cx) == self.session_id(cx) { | ||
| 285 | Some(item::Dedup::KeepExisting) | ||
| 286 | } else { | ||
| 287 | None | ||
| 288 | } | ||
| 289 | } | ||
| 290 | |||
| 291 | fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { | ||
| 292 | true | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | impl Render for DebugSession { | ||
| 297 | fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { | ||
| 298 | match &self.mode { | ||
| 299 | DebugSessionState::Inert(inert_state) => { | ||
| 300 | inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) | ||
| 301 | } | ||
| 302 | DebugSessionState::Starting(starting_state) => { | ||
| 303 | starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) | ||
| 304 | } | ||
| 305 | DebugSessionState::Failed(failed_state) => { | ||
| 306 | failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) | ||
| 307 | } | ||
| 308 | DebugSessionState::Running(running_state) => { | ||
| 309 | running_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) | ||
| 310 | } | ||
| 311 | } | ||
| 312 | } | ||
| 313 | } | ||
| @@ -0,0 +1,30 @@ | |||
| 1 | use gpui::{FocusHandle, Focusable}; | ||
| 2 | use ui::{ | ||
| 3 | h_flex, Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window, | ||
| 4 | }; | ||
| 5 | |||
| 6 | pub(crate) struct FailedState { | ||
| 7 | focus_handle: FocusHandle, | ||
| 8 | } | ||
| 9 | impl FailedState { | ||
| 10 | pub(super) fn new(cx: &mut Context<Self>) -> Self { | ||
| 11 | Self { | ||
| 12 | focus_handle: cx.focus_handle(), | ||
| 13 | } | ||
| 14 | } | ||
| 15 | } | ||
| 16 | |||
| 17 | impl Focusable for FailedState { | ||
| 18 | fn focus_handle(&self, _: &ui::App) -> FocusHandle { | ||
| 19 | self.focus_handle.clone() | ||
| 20 | } | ||
| 21 | } | ||
| 22 | impl Render for FailedState { | ||
| 23 | fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { | ||
| 24 | h_flex() | ||
| 25 | .size_full() | ||
| 26 | .items_center() | ||
| 27 | .justify_center() | ||
| 28 | .child(Label::new("Failed to spawn debugging session").color(Color::Error)) | ||
| 29 | } | ||
| 30 | } | ||
| @@ -0,0 +1,219 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType}; | ||
| 4 | use editor::{Editor, EditorElement, EditorStyle}; | ||
| 5 | use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity}; | ||
| 6 | use settings::Settings as _; | ||
| 7 | use task::TCPHost; | ||
| 8 | use theme::ThemeSettings; | ||
| 9 | use ui::{ | ||
| 10 | h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable, | ||
| 11 | Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement, | ||
| 12 | ParentElement, Render, SharedString, Styled, Window, | ||
| 13 | }; | ||
| 14 | use workspace::Workspace; | ||
| 15 | |||
| 16 | use crate::attach_modal::AttachModal; | ||
| 17 | |||
| 18 | pub(crate) struct InertState { | ||
| 19 | focus_handle: FocusHandle, | ||
| 20 | selected_debugger: Option<SharedString>, | ||
| 21 | program_editor: Entity<Editor>, | ||
| 22 | cwd_editor: Entity<Editor>, | ||
| 23 | workspace: WeakEntity<Workspace>, | ||
| 24 | } | ||
| 25 | |||
| 26 | impl InertState { | ||
| 27 | pub(super) fn new( | ||
| 28 | workspace: WeakEntity<Workspace>, | ||
| 29 | default_cwd: &str, | ||
| 30 | window: &mut Window, | ||
| 31 | cx: &mut Context<Self>, | ||
| 32 | ) -> Self { | ||
| 33 | let program_editor = cx.new(|cx| { | ||
| 34 | let mut editor = Editor::single_line(window, cx); | ||
| 35 | editor.set_placeholder_text("Program path", cx); | ||
| 36 | editor | ||
| 37 | }); | ||
| 38 | let cwd_editor = cx.new(|cx| { | ||
| 39 | let mut editor = Editor::single_line(window, cx); | ||
| 40 | editor.insert(default_cwd, window, cx); | ||
| 41 | editor.set_placeholder_text("Working directory", cx); | ||
| 42 | editor | ||
| 43 | }); | ||
| 44 | Self { | ||
| 45 | workspace, | ||
| 46 | cwd_editor, | ||
| 47 | program_editor, | ||
| 48 | selected_debugger: None, | ||
| 49 | focus_handle: cx.focus_handle(), | ||
| 50 | } | ||
| 51 | } | ||
| 52 | } | ||
| 53 | impl Focusable for InertState { | ||
| 54 | fn focus_handle(&self, _cx: &App) -> FocusHandle { | ||
| 55 | self.focus_handle.clone() | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | pub(crate) enum InertEvent { | ||
| 60 | Spawned { config: DebugAdapterConfig }, | ||
| 61 | } | ||
| 62 | |||
| 63 | impl EventEmitter<InertEvent> for InertState {} | ||
| 64 | |||
| 65 | static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); | ||
| 66 | |||
| 67 | impl Render for InertState { | ||
| 68 | fn render( | ||
| 69 | &mut self, | ||
| 70 | window: &mut ui::Window, | ||
| 71 | cx: &mut ui::Context<'_, Self>, | ||
| 72 | ) -> impl ui::IntoElement { | ||
| 73 | let weak = cx.weak_entity(); | ||
| 74 | let disable_buttons = self.selected_debugger.is_none(); | ||
| 75 | v_flex() | ||
| 76 | .track_focus(&self.focus_handle) | ||
| 77 | .size_full() | ||
| 78 | .gap_1() | ||
| 79 | .p_2() | ||
| 80 | |||
| 81 | .child( | ||
| 82 | v_flex().gap_1() | ||
| 83 | .child( | ||
| 84 | h_flex() | ||
| 85 | .w_full() | ||
| 86 | .gap_2() | ||
| 87 | |||
| 88 | .child(Self::render_editor(&self.program_editor, cx)) | ||
| 89 | .child( | ||
| 90 | h_flex().child(DropdownMenu::new( | ||
| 91 | "dap-adapter-picker", | ||
| 92 | self.selected_debugger | ||
| 93 | .as_ref() | ||
| 94 | .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL) | ||
| 95 | .clone(), | ||
| 96 | ContextMenu::build(window, cx, move |this, _, _| { | ||
| 97 | let setter_for_name = |name: &'static str| { | ||
| 98 | let weak = weak.clone(); | ||
| 99 | move |_: &mut Window, cx: &mut App| { | ||
| 100 | let name = name; | ||
| 101 | (&weak) | ||
| 102 | .update(cx, move |this, _| { | ||
| 103 | this.selected_debugger = Some(name.into()); | ||
| 104 | }) | ||
| 105 | .ok(); | ||
| 106 | } | ||
| 107 | }; | ||
| 108 | this.entry("GDB", None, setter_for_name("GDB")) | ||
| 109 | .entry("Delve", None, setter_for_name("Delve")) | ||
| 110 | .entry("LLDB", None, setter_for_name("LLDB")) | ||
| 111 | .entry("PHP", None, setter_for_name("PHP")) | ||
| 112 | .entry("JavaScript", None, setter_for_name("JavaScript")) | ||
| 113 | .entry("Debugpy", None, setter_for_name("Debugpy")) | ||
| 114 | }), | ||
| 115 | )), | ||
| 116 | ) | ||
| 117 | ) | ||
| 118 | .child( | ||
| 119 | h_flex().gap_2().child( | ||
| 120 | Self::render_editor(&self.cwd_editor, cx), | ||
| 121 | ).child(h_flex() | ||
| 122 | .gap_4() | ||
| 123 | .pl_2() | ||
| 124 | .child( | ||
| 125 | Button::new("launch-dap", "Launch") | ||
| 126 | .style(ButtonStyle::Filled) | ||
| 127 | .disabled(disable_buttons) | ||
| 128 | .on_click(cx.listener(|this, _, _, cx| { | ||
| 129 | let program = this.program_editor.read(cx).text(cx); | ||
| 130 | let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx)); | ||
| 131 | let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project"))); | ||
| 132 | cx.emit(InertEvent::Spawned { | ||
| 133 | config: DebugAdapterConfig { | ||
| 134 | label: "hard coded".into(), | ||
| 135 | kind, | ||
| 136 | request: DebugRequestType::Launch, | ||
| 137 | program: Some(program), | ||
| 138 | cwd: Some(cwd), | ||
| 139 | initialize_args: None, | ||
| 140 | supports_attach: false, | ||
| 141 | }, | ||
| 142 | }); | ||
| 143 | })), | ||
| 144 | ) | ||
| 145 | .child(Button::new("attach-dap", "Attach") | ||
| 146 | .style(ButtonStyle::Filled) | ||
| 147 | .disabled(disable_buttons) | ||
| 148 | .on_click(cx.listener(|this, _, window, cx| this.attach(window, cx))) | ||
| 149 | )) | ||
| 150 | ) | ||
| 151 | ) | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | fn kind_for_label(label: &str) -> DebugAdapterKind { | ||
| 156 | match label { | ||
| 157 | "LLDB" => DebugAdapterKind::Lldb, | ||
| 158 | "Debugpy" => DebugAdapterKind::Python(TCPHost::default()), | ||
| 159 | "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()), | ||
| 160 | "PHP" => DebugAdapterKind::Php(TCPHost::default()), | ||
| 161 | "Delve" => DebugAdapterKind::Go(TCPHost::default()), | ||
| 162 | _ => { | ||
| 163 | unimplemented!() | ||
| 164 | } // Maybe we should set a toast notification here | ||
| 165 | } | ||
| 166 | } | ||
| 167 | impl InertState { | ||
| 168 | fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement { | ||
| 169 | let settings = ThemeSettings::get_global(cx); | ||
| 170 | let text_style = TextStyle { | ||
| 171 | color: cx.theme().colors().text, | ||
| 172 | font_family: settings.buffer_font.family.clone(), | ||
| 173 | font_features: settings.buffer_font.features.clone(), | ||
| 174 | font_size: settings.buffer_font_size(cx).into(), | ||
| 175 | font_weight: settings.buffer_font.weight, | ||
| 176 | line_height: relative(settings.buffer_line_height.value()), | ||
| 177 | ..Default::default() | ||
| 178 | }; | ||
| 179 | |||
| 180 | EditorElement::new( | ||
| 181 | editor, | ||
| 182 | EditorStyle { | ||
| 183 | background: cx.theme().colors().editor_background, | ||
| 184 | local_player: cx.theme().players().local(), | ||
| 185 | text: text_style, | ||
| 186 | ..Default::default() | ||
| 187 | }, | ||
| 188 | ) | ||
| 189 | } | ||
| 190 | |||
| 191 | fn attach(&self, window: &mut Window, cx: &mut Context<Self>) { | ||
| 192 | let process_id = self.program_editor.read(cx).text(cx).parse::<u32>().ok(); | ||
| 193 | let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx)); | ||
| 194 | let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| { | ||
| 195 | unimplemented!("Automatic selection of a debugger based on users project") | ||
| 196 | })); | ||
| 197 | |||
| 198 | let config = DebugAdapterConfig { | ||
| 199 | label: "hard coded attach".into(), | ||
| 200 | kind, | ||
| 201 | request: DebugRequestType::Attach(task::AttachConfig { process_id }), | ||
| 202 | program: None, | ||
| 203 | cwd: Some(cwd), | ||
| 204 | initialize_args: None, | ||
| 205 | supports_attach: true, | ||
| 206 | }; | ||
| 207 | |||
| 208 | if process_id.is_some() { | ||
| 209 | cx.emit(InertEvent::Spawned { config }); | ||
| 210 | } else { | ||
| 211 | let _ = self.workspace.update(cx, |workspace, cx| { | ||
| 212 | let project = workspace.project().clone(); | ||
| 213 | workspace.toggle_modal(window, cx, |window, cx| { | ||
| 214 | AttachModal::new(project, config, window, cx) | ||
| 215 | }); | ||
| 216 | }); | ||
| 217 | } | ||
| 218 | } | ||
| 219 | } | ||
| @@ -0,0 +1,686 @@ | |||
| 1 | mod console; | ||
| 2 | mod loaded_source_list; | ||
| 3 | mod module_list; | ||
| 4 | pub mod stack_frame_list; | ||
| 5 | pub mod variable_list; | ||
| 6 | |||
| 7 | use super::{DebugPanelItemEvent, ThreadItem}; | ||
| 8 | use console::Console; | ||
| 9 | use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread}; | ||
| 10 | use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity}; | ||
| 11 | use loaded_source_list::LoadedSourceList; | ||
| 12 | use module_list::ModuleList; | ||
| 13 | use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}; | ||
| 14 | use rpc::proto::ViewId; | ||
| 15 | use settings::Settings; | ||
| 16 | use stack_frame_list::StackFrameList; | ||
| 17 | use ui::{ | ||
| 18 | div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context, | ||
| 19 | ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, | ||
| 20 | Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString, | ||
| 21 | StatefulInteractiveElement, Styled, Tooltip, Window, | ||
| 22 | }; | ||
| 23 | use util::ResultExt; | ||
| 24 | use variable_list::VariableList; | ||
| 25 | use workspace::Workspace; | ||
| 26 | |||
| 27 | pub struct RunningState { | ||
| 28 | session: Entity<Session>, | ||
| 29 | thread_id: Option<ThreadId>, | ||
| 30 | console: Entity<console::Console>, | ||
| 31 | focus_handle: FocusHandle, | ||
| 32 | _remote_id: Option<ViewId>, | ||
| 33 | show_console_indicator: bool, | ||
| 34 | module_list: Entity<module_list::ModuleList>, | ||
| 35 | active_thread_item: ThreadItem, | ||
| 36 | workspace: WeakEntity<Workspace>, | ||
| 37 | session_id: SessionId, | ||
| 38 | variable_list: Entity<variable_list::VariableList>, | ||
| 39 | _subscriptions: Vec<Subscription>, | ||
| 40 | stack_frame_list: Entity<stack_frame_list::StackFrameList>, | ||
| 41 | loaded_source_list: Entity<loaded_source_list::LoadedSourceList>, | ||
| 42 | } | ||
| 43 | |||
| 44 | impl Render for RunningState { | ||
| 45 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 46 | let threads = self.session.update(cx, |this, cx| this.threads(cx)); | ||
| 47 | self.select_current_thread(&threads, cx); | ||
| 48 | |||
| 49 | let thread_status = self | ||
| 50 | .thread_id | ||
| 51 | .map(|thread_id| self.session.read(cx).thread_status(thread_id)) | ||
| 52 | .unwrap_or(ThreadStatus::Exited); | ||
| 53 | |||
| 54 | let selected_thread_name = threads | ||
| 55 | .iter() | ||
| 56 | .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id)) | ||
| 57 | .map(|(thread, _)| thread.name.clone()) | ||
| 58 | .unwrap_or("Threads".to_owned()); | ||
| 59 | |||
| 60 | self.variable_list.update(cx, |this, cx| { | ||
| 61 | this.disabled(thread_status != ThreadStatus::Stopped, cx); | ||
| 62 | }); | ||
| 63 | |||
| 64 | let is_terminated = self.session.read(cx).is_terminated(); | ||
| 65 | let active_thread_item = &self.active_thread_item; | ||
| 66 | |||
| 67 | let has_no_threads = threads.is_empty(); | ||
| 68 | let capabilities = self.capabilities(cx); | ||
| 69 | let state = cx.entity(); | ||
| 70 | h_flex() | ||
| 71 | .when(is_terminated, |this| this.bg(gpui::red())) | ||
| 72 | .key_context("DebugPanelItem") | ||
| 73 | .track_focus(&self.focus_handle(cx)) | ||
| 74 | .size_full() | ||
| 75 | .items_start() | ||
| 76 | .child( | ||
| 77 | v_flex() | ||
| 78 | .size_full() | ||
| 79 | .items_start() | ||
| 80 | .child( | ||
| 81 | h_flex() | ||
| 82 | .w_full() | ||
| 83 | .border_b_1() | ||
| 84 | .border_color(cx.theme().colors().border_variant) | ||
| 85 | .justify_between() | ||
| 86 | .child( | ||
| 87 | h_flex() | ||
| 88 | .p_1() | ||
| 89 | .w_full() | ||
| 90 | .gap_2() | ||
| 91 | .map(|this| { | ||
| 92 | if thread_status == ThreadStatus::Running { | ||
| 93 | this.child( | ||
| 94 | IconButton::new( | ||
| 95 | "debug-pause", | ||
| 96 | IconName::DebugPause, | ||
| 97 | ) | ||
| 98 | .icon_size(IconSize::Small) | ||
| 99 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 100 | this.pause_thread(cx); | ||
| 101 | })) | ||
| 102 | .tooltip(move |window, cx| { | ||
| 103 | Tooltip::text("Pause program")(window, cx) | ||
| 104 | }), | ||
| 105 | ) | ||
| 106 | } else { | ||
| 107 | this.child( | ||
| 108 | IconButton::new( | ||
| 109 | "debug-continue", | ||
| 110 | IconName::DebugContinue, | ||
| 111 | ) | ||
| 112 | .icon_size(IconSize::Small) | ||
| 113 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 114 | this.continue_thread(cx) | ||
| 115 | })) | ||
| 116 | .disabled(thread_status != ThreadStatus::Stopped) | ||
| 117 | .tooltip(move |window, cx| { | ||
| 118 | Tooltip::text("Continue program")(window, cx) | ||
| 119 | }), | ||
| 120 | ) | ||
| 121 | } | ||
| 122 | }) | ||
| 123 | .when( | ||
| 124 | capabilities.supports_step_back.unwrap_or(false), | ||
| 125 | |this| { | ||
| 126 | this.child( | ||
| 127 | IconButton::new( | ||
| 128 | "debug-step-back", | ||
| 129 | IconName::DebugStepBack, | ||
| 130 | ) | ||
| 131 | .icon_size(IconSize::Small) | ||
| 132 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 133 | this.step_back(cx); | ||
| 134 | })) | ||
| 135 | .disabled(thread_status != ThreadStatus::Stopped) | ||
| 136 | .tooltip(move |window, cx| { | ||
| 137 | Tooltip::text("Step back")(window, cx) | ||
| 138 | }), | ||
| 139 | ) | ||
| 140 | }, | ||
| 141 | ) | ||
| 142 | .child( | ||
| 143 | IconButton::new("debug-step-over", IconName::DebugStepOver) | ||
| 144 | .icon_size(IconSize::Small) | ||
| 145 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 146 | this.step_over(cx); | ||
| 147 | })) | ||
| 148 | .disabled(thread_status != ThreadStatus::Stopped) | ||
| 149 | .tooltip(move |window, cx| { | ||
| 150 | Tooltip::text("Step over")(window, cx) | ||
| 151 | }), | ||
| 152 | ) | ||
| 153 | .child( | ||
| 154 | IconButton::new("debug-step-in", IconName::DebugStepInto) | ||
| 155 | .icon_size(IconSize::Small) | ||
| 156 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 157 | this.step_in(cx); | ||
| 158 | })) | ||
| 159 | .disabled(thread_status != ThreadStatus::Stopped) | ||
| 160 | .tooltip(move |window, cx| { | ||
| 161 | Tooltip::text("Step in")(window, cx) | ||
| 162 | }), | ||
| 163 | ) | ||
| 164 | .child( | ||
| 165 | IconButton::new("debug-step-out", IconName::DebugStepOut) | ||
| 166 | .icon_size(IconSize::Small) | ||
| 167 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 168 | this.step_out(cx); | ||
| 169 | })) | ||
| 170 | .disabled(thread_status != ThreadStatus::Stopped) | ||
| 171 | .tooltip(move |window, cx| { | ||
| 172 | Tooltip::text("Step out")(window, cx) | ||
| 173 | }), | ||
| 174 | ) | ||
| 175 | .child( | ||
| 176 | IconButton::new("debug-restart", IconName::DebugRestart) | ||
| 177 | .icon_size(IconSize::Small) | ||
| 178 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 179 | this.restart_session(cx); | ||
| 180 | })) | ||
| 181 | .disabled( | ||
| 182 | !capabilities | ||
| 183 | .supports_restart_request | ||
| 184 | .unwrap_or_default(), | ||
| 185 | ) | ||
| 186 | .tooltip(move |window, cx| { | ||
| 187 | Tooltip::text("Restart")(window, cx) | ||
| 188 | }), | ||
| 189 | ) | ||
| 190 | .child( | ||
| 191 | IconButton::new("debug-stop", IconName::DebugStop) | ||
| 192 | .icon_size(IconSize::Small) | ||
| 193 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 194 | this.stop_thread(cx); | ||
| 195 | })) | ||
| 196 | .disabled( | ||
| 197 | thread_status != ThreadStatus::Stopped | ||
| 198 | && thread_status != ThreadStatus::Running, | ||
| 199 | ) | ||
| 200 | .tooltip({ | ||
| 201 | let label = if capabilities | ||
| 202 | .supports_terminate_threads_request | ||
| 203 | .unwrap_or_default() | ||
| 204 | { | ||
| 205 | "Terminate Thread" | ||
| 206 | } else { | ||
| 207 | "Terminate all Threads" | ||
| 208 | }; | ||
| 209 | move |window, cx| Tooltip::text(label)(window, cx) | ||
| 210 | }), | ||
| 211 | ) | ||
| 212 | .child( | ||
| 213 | IconButton::new( | ||
| 214 | "debug-disconnect", | ||
| 215 | IconName::DebugDisconnect, | ||
| 216 | ) | ||
| 217 | .icon_size(IconSize::Small) | ||
| 218 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 219 | this.disconnect_client(cx); | ||
| 220 | })) | ||
| 221 | .disabled( | ||
| 222 | thread_status == ThreadStatus::Exited | ||
| 223 | || thread_status == ThreadStatus::Ended, | ||
| 224 | ) | ||
| 225 | .tooltip( | ||
| 226 | move |window, cx| { | ||
| 227 | Tooltip::text("Disconnect")(window, cx) | ||
| 228 | }, | ||
| 229 | ), | ||
| 230 | ) | ||
| 231 | .child( | ||
| 232 | IconButton::new( | ||
| 233 | "debug-ignore-breakpoints", | ||
| 234 | if self.session.read(cx).breakpoints_enabled() { | ||
| 235 | IconName::DebugBreakpoint | ||
| 236 | } else { | ||
| 237 | IconName::DebugIgnoreBreakpoints | ||
| 238 | }, | ||
| 239 | ) | ||
| 240 | .icon_size(IconSize::Small) | ||
| 241 | .on_click(cx.listener(|this, _, _window, cx| { | ||
| 242 | this.toggle_ignore_breakpoints(cx); | ||
| 243 | })) | ||
| 244 | .disabled( | ||
| 245 | thread_status == ThreadStatus::Exited | ||
| 246 | || thread_status == ThreadStatus::Ended, | ||
| 247 | ) | ||
| 248 | .tooltip( | ||
| 249 | move |window, cx| { | ||
| 250 | Tooltip::text("Ignore breakpoints")(window, cx) | ||
| 251 | }, | ||
| 252 | ), | ||
| 253 | ), | ||
| 254 | ) | ||
| 255 | //.child(h_flex()) | ||
| 256 | .child( | ||
| 257 | h_flex().p_1().mx_2().w_3_4().justify_end().child( | ||
| 258 | DropdownMenu::new( | ||
| 259 | ("thread-list", self.session_id.0), | ||
| 260 | selected_thread_name, | ||
| 261 | ContextMenu::build(window, cx, move |mut this, _, _| { | ||
| 262 | for (thread, _) in threads { | ||
| 263 | let state = state.clone(); | ||
| 264 | let thread_id = thread.id; | ||
| 265 | this = | ||
| 266 | this.entry(thread.name, None, move |_, cx| { | ||
| 267 | state.update(cx, |state, cx| { | ||
| 268 | state.select_thread( | ||
| 269 | ThreadId(thread_id), | ||
| 270 | cx, | ||
| 271 | ); | ||
| 272 | }); | ||
| 273 | }); | ||
| 274 | } | ||
| 275 | this | ||
| 276 | }), | ||
| 277 | ) | ||
| 278 | .disabled( | ||
| 279 | has_no_threads || thread_status != ThreadStatus::Stopped, | ||
| 280 | ), | ||
| 281 | ), | ||
| 282 | ), | ||
| 283 | ) | ||
| 284 | .child( | ||
| 285 | h_flex() | ||
| 286 | .size_full() | ||
| 287 | .items_start() | ||
| 288 | .p_1() | ||
| 289 | .gap_4() | ||
| 290 | .child(self.stack_frame_list.clone()), | ||
| 291 | ), | ||
| 292 | ) | ||
| 293 | .child( | ||
| 294 | v_flex() | ||
| 295 | .border_l_1() | ||
| 296 | .border_color(cx.theme().colors().border_variant) | ||
| 297 | .size_full() | ||
| 298 | .items_start() | ||
| 299 | .child( | ||
| 300 | h_flex() | ||
| 301 | .border_b_1() | ||
| 302 | .w_full() | ||
| 303 | .border_color(cx.theme().colors().border_variant) | ||
| 304 | .child(self.render_entry_button( | ||
| 305 | &SharedString::from("Variables"), | ||
| 306 | ThreadItem::Variables, | ||
| 307 | cx, | ||
| 308 | )) | ||
| 309 | .when( | ||
| 310 | capabilities.supports_modules_request.unwrap_or_default(), | ||
| 311 | |this| { | ||
| 312 | this.child(self.render_entry_button( | ||
| 313 | &SharedString::from("Modules"), | ||
| 314 | ThreadItem::Modules, | ||
| 315 | cx, | ||
| 316 | )) | ||
| 317 | }, | ||
| 318 | ) | ||
| 319 | .when( | ||
| 320 | capabilities | ||
| 321 | .supports_loaded_sources_request | ||
| 322 | .unwrap_or_default(), | ||
| 323 | |this| { | ||
| 324 | this.child(self.render_entry_button( | ||
| 325 | &SharedString::from("Loaded Sources"), | ||
| 326 | ThreadItem::LoadedSource, | ||
| 327 | cx, | ||
| 328 | )) | ||
| 329 | }, | ||
| 330 | ) | ||
| 331 | .child(self.render_entry_button( | ||
| 332 | &SharedString::from("Console"), | ||
| 333 | ThreadItem::Console, | ||
| 334 | cx, | ||
| 335 | )), | ||
| 336 | ) | ||
| 337 | .when(*active_thread_item == ThreadItem::Variables, |this| { | ||
| 338 | this.child(self.variable_list.clone()) | ||
| 339 | }) | ||
| 340 | .when(*active_thread_item == ThreadItem::Modules, |this| { | ||
| 341 | this.size_full().child(self.module_list.clone()) | ||
| 342 | }) | ||
| 343 | .when(*active_thread_item == ThreadItem::LoadedSource, |this| { | ||
| 344 | this.size_full().child(self.loaded_source_list.clone()) | ||
| 345 | }) | ||
| 346 | .when(*active_thread_item == ThreadItem::Console, |this| { | ||
| 347 | this.child(self.console.clone()) | ||
| 348 | }), | ||
| 349 | ) | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | impl RunningState { | ||
| 354 | pub fn new( | ||
| 355 | session: Entity<Session>, | ||
| 356 | workspace: WeakEntity<Workspace>, | ||
| 357 | window: &mut Window, | ||
| 358 | cx: &mut Context<Self>, | ||
| 359 | ) -> Self { | ||
| 360 | let focus_handle = cx.focus_handle(); | ||
| 361 | let session_id = session.read(cx).session_id(); | ||
| 362 | let weak_state = cx.weak_entity(); | ||
| 363 | let stack_frame_list = cx.new(|cx| { | ||
| 364 | StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx) | ||
| 365 | }); | ||
| 366 | |||
| 367 | let variable_list = | ||
| 368 | cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); | ||
| 369 | |||
| 370 | let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx)); | ||
| 371 | |||
| 372 | let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); | ||
| 373 | |||
| 374 | let console = cx.new(|cx| { | ||
| 375 | Console::new( | ||
| 376 | session.clone(), | ||
| 377 | stack_frame_list.clone(), | ||
| 378 | variable_list.clone(), | ||
| 379 | window, | ||
| 380 | cx, | ||
| 381 | ) | ||
| 382 | }); | ||
| 383 | |||
| 384 | let _subscriptions = vec![ | ||
| 385 | cx.observe(&module_list, |_, _, cx| cx.notify()), | ||
| 386 | cx.subscribe_in(&session, window, |this, _, event, window, cx| { | ||
| 387 | match event { | ||
| 388 | SessionEvent::Stopped(thread_id) => { | ||
| 389 | this.workspace | ||
| 390 | .update(cx, |workspace, cx| { | ||
| 391 | workspace.open_panel::<crate::DebugPanel>(window, cx); | ||
| 392 | }) | ||
| 393 | .log_err(); | ||
| 394 | |||
| 395 | if let Some(thread_id) = thread_id { | ||
| 396 | this.select_thread(*thread_id, cx); | ||
| 397 | } | ||
| 398 | } | ||
| 399 | SessionEvent::Threads => { | ||
| 400 | let threads = this.session.update(cx, |this, cx| this.threads(cx)); | ||
| 401 | this.select_current_thread(&threads, cx); | ||
| 402 | } | ||
| 403 | _ => {} | ||
| 404 | } | ||
| 405 | cx.notify() | ||
| 406 | }), | ||
| 407 | ]; | ||
| 408 | |||
| 409 | Self { | ||
| 410 | session, | ||
| 411 | console, | ||
| 412 | workspace, | ||
| 413 | module_list, | ||
| 414 | focus_handle, | ||
| 415 | variable_list, | ||
| 416 | _subscriptions, | ||
| 417 | thread_id: None, | ||
| 418 | _remote_id: None, | ||
| 419 | stack_frame_list, | ||
| 420 | loaded_source_list, | ||
| 421 | session_id, | ||
| 422 | show_console_indicator: false, | ||
| 423 | active_thread_item: ThreadItem::Variables, | ||
| 424 | } | ||
| 425 | } | ||
| 426 | |||
| 427 | pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) { | ||
| 428 | if self.thread_id.is_some() { | ||
| 429 | self.stack_frame_list | ||
| 430 | .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx)); | ||
| 431 | } | ||
| 432 | } | ||
| 433 | |||
| 434 | pub fn session(&self) -> &Entity<Session> { | ||
| 435 | &self.session | ||
| 436 | } | ||
| 437 | |||
| 438 | pub fn session_id(&self) -> SessionId { | ||
| 439 | self.session_id | ||
| 440 | } | ||
| 441 | |||
| 442 | #[cfg(any(test, feature = "test-support"))] | ||
| 443 | pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) { | ||
| 444 | self.active_thread_item = thread_item; | ||
| 445 | cx.notify() | ||
| 446 | } | ||
| 447 | |||
| 448 | #[cfg(any(test, feature = "test-support"))] | ||
| 449 | pub fn stack_frame_list(&self) -> &Entity<StackFrameList> { | ||
| 450 | &self.stack_frame_list | ||
| 451 | } | ||
| 452 | |||
| 453 | #[cfg(any(test, feature = "test-support"))] | ||
| 454 | pub fn console(&self) -> &Entity<Console> { | ||
| 455 | &self.console | ||
| 456 | } | ||
| 457 | |||
| 458 | #[cfg(any(test, feature = "test-support"))] | ||
| 459 | pub fn module_list(&self) -> &Entity<ModuleList> { | ||
| 460 | &self.module_list | ||
| 461 | } | ||
| 462 | |||
| 463 | #[cfg(any(test, feature = "test-support"))] | ||
| 464 | pub fn variable_list(&self) -> &Entity<VariableList> { | ||
| 465 | &self.variable_list | ||
| 466 | } | ||
| 467 | |||
| 468 | #[cfg(any(test, feature = "test-support"))] | ||
| 469 | pub fn are_breakpoints_ignored(&self, cx: &App) -> bool { | ||
| 470 | self.session.read(cx).ignore_breakpoints() | ||
| 471 | } | ||
| 472 | |||
| 473 | pub fn capabilities(&self, cx: &App) -> Capabilities { | ||
| 474 | self.session().read(cx).capabilities().clone() | ||
| 475 | } | ||
| 476 | |||
| 477 | pub fn select_current_thread( | ||
| 478 | &mut self, | ||
| 479 | threads: &Vec<(Thread, ThreadStatus)>, | ||
| 480 | cx: &mut Context<Self>, | ||
| 481 | ) { | ||
| 482 | let selected_thread = self | ||
| 483 | .thread_id | ||
| 484 | .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0)) | ||
| 485 | .or_else(|| threads.first()); | ||
| 486 | |||
| 487 | let Some((selected_thread, _)) = selected_thread else { | ||
| 488 | return; | ||
| 489 | }; | ||
| 490 | |||
| 491 | if Some(ThreadId(selected_thread.id)) != self.thread_id { | ||
| 492 | self.select_thread(ThreadId(selected_thread.id), cx); | ||
| 493 | } | ||
| 494 | } | ||
| 495 | |||
| 496 | #[cfg(any(test, feature = "test-support"))] | ||
| 497 | pub fn selected_thread_id(&self) -> Option<ThreadId> { | ||
| 498 | self.thread_id | ||
| 499 | } | ||
| 500 | |||
| 501 | pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> { | ||
| 502 | self.thread_id | ||
| 503 | .map(|id| self.session().read(cx).thread_status(id)) | ||
| 504 | } | ||
| 505 | |||
| 506 | fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) { | ||
| 507 | if self.thread_id.is_some_and(|id| id == thread_id) { | ||
| 508 | return; | ||
| 509 | } | ||
| 510 | |||
| 511 | self.thread_id = Some(thread_id); | ||
| 512 | |||
| 513 | self.stack_frame_list | ||
| 514 | .update(cx, |list, cx| list.refresh(cx)); | ||
| 515 | cx.notify(); | ||
| 516 | } | ||
| 517 | |||
| 518 | fn render_entry_button( | ||
| 519 | &self, | ||
| 520 | label: &SharedString, | ||
| 521 | thread_item: ThreadItem, | ||
| 522 | cx: &mut Context<Self>, | ||
| 523 | ) -> AnyElement { | ||
| 524 | let has_indicator = | ||
| 525 | matches!(thread_item, ThreadItem::Console) && self.show_console_indicator; | ||
| 526 | |||
| 527 | div() | ||
| 528 | .id(label.clone()) | ||
| 529 | .px_2() | ||
| 530 | .py_1() | ||
| 531 | .cursor_pointer() | ||
| 532 | .border_b_2() | ||
| 533 | .when(self.active_thread_item == thread_item, |this| { | ||
| 534 | this.border_color(cx.theme().colors().border) | ||
| 535 | }) | ||
| 536 | .child( | ||
| 537 | h_flex() | ||
| 538 | .child(Button::new(label.clone(), label.clone())) | ||
| 539 | .when(has_indicator, |this| this.child(Indicator::dot())), | ||
| 540 | ) | ||
| 541 | .on_click(cx.listener(move |this, _, _window, cx| { | ||
| 542 | this.active_thread_item = thread_item; | ||
| 543 | |||
| 544 | if matches!(this.active_thread_item, ThreadItem::Console) { | ||
| 545 | this.show_console_indicator = false; | ||
| 546 | } | ||
| 547 | |||
| 548 | cx.notify(); | ||
| 549 | })) | ||
| 550 | .into_any_element() | ||
| 551 | } | ||
| 552 | |||
| 553 | pub fn continue_thread(&mut self, cx: &mut Context<Self>) { | ||
| 554 | let Some(thread_id) = self.thread_id else { | ||
| 555 | return; | ||
| 556 | }; | ||
| 557 | |||
| 558 | self.session().update(cx, |state, cx| { | ||
| 559 | state.continue_thread(thread_id, cx); | ||
| 560 | }); | ||
| 561 | } | ||
| 562 | |||
| 563 | pub fn step_over(&mut self, cx: &mut Context<Self>) { | ||
| 564 | let Some(thread_id) = self.thread_id else { | ||
| 565 | return; | ||
| 566 | }; | ||
| 567 | |||
| 568 | let granularity = DebuggerSettings::get_global(cx).stepping_granularity; | ||
| 569 | |||
| 570 | self.session().update(cx, |state, cx| { | ||
| 571 | state.step_over(thread_id, granularity, cx); | ||
| 572 | }); | ||
| 573 | } | ||
| 574 | |||
| 575 | pub fn step_in(&mut self, cx: &mut Context<Self>) { | ||
| 576 | let Some(thread_id) = self.thread_id else { | ||
| 577 | return; | ||
| 578 | }; | ||
| 579 | |||
| 580 | let granularity = DebuggerSettings::get_global(cx).stepping_granularity; | ||
| 581 | |||
| 582 | self.session().update(cx, |state, cx| { | ||
| 583 | state.step_in(thread_id, granularity, cx); | ||
| 584 | }); | ||
| 585 | } | ||
| 586 | |||
| 587 | pub fn step_out(&mut self, cx: &mut Context<Self>) { | ||
| 588 | let Some(thread_id) = self.thread_id else { | ||
| 589 | return; | ||
| 590 | }; | ||
| 591 | |||
| 592 | let granularity = DebuggerSettings::get_global(cx).stepping_granularity; | ||
| 593 | |||
| 594 | self.session().update(cx, |state, cx| { | ||
| 595 | state.step_out(thread_id, granularity, cx); | ||
| 596 | }); | ||
| 597 | } | ||
| 598 | |||
| 599 | pub fn step_back(&mut self, cx: &mut Context<Self>) { | ||
| 600 | let Some(thread_id) = self.thread_id else { | ||
| 601 | return; | ||
| 602 | }; | ||
| 603 | |||
| 604 | let granularity = DebuggerSettings::get_global(cx).stepping_granularity; | ||
| 605 | |||
| 606 | self.session().update(cx, |state, cx| { | ||
| 607 | state.step_back(thread_id, granularity, cx); | ||
| 608 | }); | ||
| 609 | } | ||
| 610 | |||
| 611 | pub fn restart_session(&self, cx: &mut Context<Self>) { | ||
| 612 | self.session().update(cx, |state, cx| { | ||
| 613 | state.restart(None, cx); | ||
| 614 | }); | ||
| 615 | } | ||
| 616 | |||
| 617 | pub fn pause_thread(&self, cx: &mut Context<Self>) { | ||
| 618 | let Some(thread_id) = self.thread_id else { | ||
| 619 | return; | ||
| 620 | }; | ||
| 621 | |||
| 622 | self.session().update(cx, |state, cx| { | ||
| 623 | state.pause_thread(thread_id, cx); | ||
| 624 | }); | ||
| 625 | } | ||
| 626 | |||
| 627 | pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) { | ||
| 628 | self.workspace | ||
| 629 | .update(cx, |workspace, cx| { | ||
| 630 | workspace | ||
| 631 | .project() | ||
| 632 | .read(cx) | ||
| 633 | .breakpoint_store() | ||
| 634 | .update(cx, |store, cx| { | ||
| 635 | store.remove_active_position(Some(self.session_id), cx) | ||
| 636 | }) | ||
| 637 | }) | ||
| 638 | .log_err(); | ||
| 639 | |||
| 640 | self.session.update(cx, |session, cx| { | ||
| 641 | session.shutdown(cx).detach(); | ||
| 642 | }) | ||
| 643 | } | ||
| 644 | |||
| 645 | pub fn stop_thread(&self, cx: &mut Context<Self>) { | ||
| 646 | let Some(thread_id) = self.thread_id else { | ||
| 647 | return; | ||
| 648 | }; | ||
| 649 | |||
| 650 | self.workspace | ||
| 651 | .update(cx, |workspace, cx| { | ||
| 652 | workspace | ||
| 653 | .project() | ||
| 654 | .read(cx) | ||
| 655 | .breakpoint_store() | ||
| 656 | .update(cx, |store, cx| { | ||
| 657 | store.remove_active_position(Some(self.session_id), cx) | ||
| 658 | }) | ||
| 659 | }) | ||
| 660 | .log_err(); | ||
| 661 | |||
| 662 | self.session().update(cx, |state, cx| { | ||
| 663 | state.terminate_threads(Some(vec![thread_id; 1]), cx); | ||
| 664 | }); | ||
| 665 | } | ||
| 666 | |||
| 667 | pub fn disconnect_client(&self, cx: &mut Context<Self>) { | ||
| 668 | self.session().update(cx, |state, cx| { | ||
| 669 | state.disconnect_client(cx); | ||
| 670 | }); | ||
| 671 | } | ||
| 672 | |||
| 673 | pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) { | ||
| 674 | self.session.update(cx, |session, cx| { | ||
| 675 | session.toggle_ignore_breakpoints(cx).detach(); | ||
| 676 | }); | ||
| 677 | } | ||
| 678 | } | ||
| 679 | |||
| 680 | impl EventEmitter<DebugPanelItemEvent> for RunningState {} | ||
| 681 | |||
| 682 | impl Focusable for RunningState { | ||
| 683 | fn focus_handle(&self, _: &App) -> FocusHandle { | ||
| 684 | self.focus_handle.clone() | ||
| 685 | } | ||
| 686 | } | ||
| @@ -0,0 +1,419 @@ | |||
| 1 | use super::{ | ||
| 2 | stack_frame_list::{StackFrameList, StackFrameListEvent}, | ||
| 3 | variable_list::VariableList, | ||
| 4 | }; | ||
| 5 | use anyhow::Result; | ||
| 6 | use collections::HashMap; | ||
| 7 | use dap::OutputEvent; | ||
| 8 | use editor::{CompletionProvider, Editor, EditorElement, EditorStyle}; | ||
| 9 | use fuzzy::StringMatchCandidate; | ||
| 10 | use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; | ||
| 11 | use language::{Buffer, CodeLabel}; | ||
| 12 | use menu::Confirm; | ||
| 13 | use project::{ | ||
| 14 | debugger::session::{CompletionsQuery, OutputToken, Session}, | ||
| 15 | Completion, | ||
| 16 | }; | ||
| 17 | use settings::Settings; | ||
| 18 | use std::{cell::RefCell, rc::Rc, usize}; | ||
| 19 | use theme::ThemeSettings; | ||
| 20 | use ui::prelude::*; | ||
| 21 | |||
| 22 | pub struct Console { | ||
| 23 | console: Entity<Editor>, | ||
| 24 | query_bar: Entity<Editor>, | ||
| 25 | session: Entity<Session>, | ||
| 26 | _subscriptions: Vec<Subscription>, | ||
| 27 | variable_list: Entity<VariableList>, | ||
| 28 | stack_frame_list: Entity<StackFrameList>, | ||
| 29 | last_token: OutputToken, | ||
| 30 | update_output_task: Task<()>, | ||
| 31 | } | ||
| 32 | |||
| 33 | impl Console { | ||
| 34 | pub fn new( | ||
| 35 | session: Entity<Session>, | ||
| 36 | stack_frame_list: Entity<StackFrameList>, | ||
| 37 | variable_list: Entity<VariableList>, | ||
| 38 | window: &mut Window, | ||
| 39 | cx: &mut Context<Self>, | ||
| 40 | ) -> Self { | ||
| 41 | let console = cx.new(|cx| { | ||
| 42 | let mut editor = Editor::multi_line(window, cx); | ||
| 43 | editor.move_to_end(&editor::actions::MoveToEnd, window, cx); | ||
| 44 | editor.set_read_only(true); | ||
| 45 | editor.set_show_gutter(true, cx); | ||
| 46 | editor.set_show_runnables(false, cx); | ||
| 47 | editor.set_show_breakpoints(false, cx); | ||
| 48 | editor.set_show_code_actions(false, cx); | ||
| 49 | editor.set_show_line_numbers(false, cx); | ||
| 50 | editor.set_show_git_diff_gutter(false, cx); | ||
| 51 | editor.set_autoindent(false); | ||
| 52 | editor.set_input_enabled(false); | ||
| 53 | editor.set_use_autoclose(false); | ||
| 54 | editor.set_show_wrap_guides(false, cx); | ||
| 55 | editor.set_show_indent_guides(false, cx); | ||
| 56 | editor.set_show_edit_predictions(Some(false), window, cx); | ||
| 57 | editor | ||
| 58 | }); | ||
| 59 | |||
| 60 | let this = cx.weak_entity(); | ||
| 61 | let query_bar = cx.new(|cx| { | ||
| 62 | let mut editor = Editor::single_line(window, cx); | ||
| 63 | editor.set_placeholder_text("Evaluate an expression", cx); | ||
| 64 | editor.set_use_autoclose(false); | ||
| 65 | editor.set_show_gutter(false, cx); | ||
| 66 | editor.set_show_wrap_guides(false, cx); | ||
| 67 | editor.set_show_indent_guides(false, cx); | ||
| 68 | editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this)))); | ||
| 69 | |||
| 70 | editor | ||
| 71 | }); | ||
| 72 | |||
| 73 | let _subscriptions = | ||
| 74 | vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)]; | ||
| 75 | |||
| 76 | Self { | ||
| 77 | session, | ||
| 78 | console, | ||
| 79 | query_bar, | ||
| 80 | variable_list, | ||
| 81 | _subscriptions, | ||
| 82 | stack_frame_list, | ||
| 83 | update_output_task: Task::ready(()), | ||
| 84 | last_token: OutputToken(0), | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | #[cfg(any(test, feature = "test-support"))] | ||
| 89 | pub fn editor(&self) -> &Entity<Editor> { | ||
| 90 | &self.console | ||
| 91 | } | ||
| 92 | |||
| 93 | #[cfg(any(test, feature = "test-support"))] | ||
| 94 | pub fn query_bar(&self) -> &Entity<Editor> { | ||
| 95 | &self.query_bar | ||
| 96 | } | ||
| 97 | |||
| 98 | fn is_local(&self, cx: &Context<Self>) -> bool { | ||
| 99 | self.session.read(cx).is_local() | ||
| 100 | } | ||
| 101 | |||
| 102 | fn handle_stack_frame_list_events( | ||
| 103 | &mut self, | ||
| 104 | _: Entity<StackFrameList>, | ||
| 105 | event: &StackFrameListEvent, | ||
| 106 | cx: &mut Context<Self>, | ||
| 107 | ) { | ||
| 108 | match event { | ||
| 109 | StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(), | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | pub fn add_messages<'a>( | ||
| 114 | &mut self, | ||
| 115 | events: impl Iterator<Item = &'a OutputEvent>, | ||
| 116 | window: &mut Window, | ||
| 117 | cx: &mut App, | ||
| 118 | ) { | ||
| 119 | self.console.update(cx, |console, cx| { | ||
| 120 | let mut to_insert = String::default(); | ||
| 121 | for event in events { | ||
| 122 | use std::fmt::Write; | ||
| 123 | |||
| 124 | _ = write!(to_insert, "{}\n", event.output.trim_end()); | ||
| 125 | } | ||
| 126 | |||
| 127 | console.set_read_only(false); | ||
| 128 | console.move_to_end(&editor::actions::MoveToEnd, window, cx); | ||
| 129 | console.insert(&to_insert, window, cx); | ||
| 130 | console.set_read_only(true); | ||
| 131 | |||
| 132 | cx.notify(); | ||
| 133 | }); | ||
| 134 | } | ||
| 135 | |||
| 136 | pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) { | ||
| 137 | let expression = self.query_bar.update(cx, |editor, cx| { | ||
| 138 | let expression = editor.text(cx); | ||
| 139 | |||
| 140 | editor.clear(window, cx); | ||
| 141 | |||
| 142 | expression | ||
| 143 | }); | ||
| 144 | |||
| 145 | self.session.update(cx, |state, cx| { | ||
| 146 | state.evaluate( | ||
| 147 | expression, | ||
| 148 | Some(dap::EvaluateArgumentsContext::Variables), | ||
| 149 | self.stack_frame_list.read(cx).current_stack_frame_id(), | ||
| 150 | None, | ||
| 151 | cx, | ||
| 152 | ); | ||
| 153 | }); | ||
| 154 | } | ||
| 155 | |||
| 156 | fn render_console(&self, cx: &Context<Self>) -> impl IntoElement { | ||
| 157 | let settings = ThemeSettings::get_global(cx); | ||
| 158 | let text_style = TextStyle { | ||
| 159 | color: if self.console.read(cx).read_only(cx) { | ||
| 160 | cx.theme().colors().text_disabled | ||
| 161 | } else { | ||
| 162 | cx.theme().colors().text | ||
| 163 | }, | ||
| 164 | font_family: settings.buffer_font.family.clone(), | ||
| 165 | font_features: settings.buffer_font.features.clone(), | ||
| 166 | font_size: settings.buffer_font_size(cx).into(), | ||
| 167 | font_weight: settings.buffer_font.weight, | ||
| 168 | line_height: relative(settings.buffer_line_height.value()), | ||
| 169 | ..Default::default() | ||
| 170 | }; | ||
| 171 | |||
| 172 | EditorElement::new( | ||
| 173 | &self.console, | ||
| 174 | EditorStyle { | ||
| 175 | background: cx.theme().colors().editor_background, | ||
| 176 | local_player: cx.theme().players().local(), | ||
| 177 | text: text_style, | ||
| 178 | ..Default::default() | ||
| 179 | }, | ||
| 180 | ) | ||
| 181 | } | ||
| 182 | |||
| 183 | fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement { | ||
| 184 | let settings = ThemeSettings::get_global(cx); | ||
| 185 | let text_style = TextStyle { | ||
| 186 | color: if self.console.read(cx).read_only(cx) { | ||
| 187 | cx.theme().colors().text_disabled | ||
| 188 | } else { | ||
| 189 | cx.theme().colors().text | ||
| 190 | }, | ||
| 191 | font_family: settings.ui_font.family.clone(), | ||
| 192 | font_features: settings.ui_font.features.clone(), | ||
| 193 | font_fallbacks: settings.ui_font.fallbacks.clone(), | ||
| 194 | font_size: TextSize::Editor.rems(cx).into(), | ||
| 195 | font_weight: settings.ui_font.weight, | ||
| 196 | line_height: relative(1.3), | ||
| 197 | ..Default::default() | ||
| 198 | }; | ||
| 199 | |||
| 200 | EditorElement::new( | ||
| 201 | &self.query_bar, | ||
| 202 | EditorStyle { | ||
| 203 | background: cx.theme().colors().editor_background, | ||
| 204 | local_player: cx.theme().players().local(), | ||
| 205 | text: text_style, | ||
| 206 | ..Default::default() | ||
| 207 | }, | ||
| 208 | ) | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | impl Render for Console { | ||
| 213 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 214 | let session = self.session.clone(); | ||
| 215 | let token = self.last_token; | ||
| 216 | self.update_output_task = cx.spawn_in(window, move |this, mut cx| async move { | ||
| 217 | _ = session.update_in(&mut cx, move |session, window, cx| { | ||
| 218 | let (output, last_processed_token) = session.output(token); | ||
| 219 | |||
| 220 | _ = this.update(cx, |this, cx| { | ||
| 221 | if last_processed_token == this.last_token { | ||
| 222 | return; | ||
| 223 | } | ||
| 224 | this.add_messages(output, window, cx); | ||
| 225 | |||
| 226 | this.last_token = last_processed_token; | ||
| 227 | }); | ||
| 228 | }); | ||
| 229 | }); | ||
| 230 | |||
| 231 | v_flex() | ||
| 232 | .key_context("DebugConsole") | ||
| 233 | .on_action(cx.listener(Self::evaluate)) | ||
| 234 | .size_full() | ||
| 235 | .child(self.render_console(cx)) | ||
| 236 | .when(self.is_local(cx), |this| { | ||
| 237 | this.child(self.render_query_bar(cx)) | ||
| 238 | .pt(DynamicSpacing::Base04.rems(cx)) | ||
| 239 | }) | ||
| 240 | .border_2() | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>); | ||
| 245 | |||
| 246 | impl CompletionProvider for ConsoleQueryBarCompletionProvider { | ||
| 247 | fn completions( | ||
| 248 | &self, | ||
| 249 | buffer: &Entity<Buffer>, | ||
| 250 | buffer_position: language::Anchor, | ||
| 251 | _trigger: editor::CompletionContext, | ||
| 252 | _window: &mut Window, | ||
| 253 | cx: &mut Context<Editor>, | ||
| 254 | ) -> Task<Result<Option<Vec<Completion>>>> { | ||
| 255 | let Some(console) = self.0.upgrade() else { | ||
| 256 | return Task::ready(Ok(None)); | ||
| 257 | }; | ||
| 258 | |||
| 259 | let support_completions = console | ||
| 260 | .read(cx) | ||
| 261 | .session | ||
| 262 | .read(cx) | ||
| 263 | .capabilities() | ||
| 264 | .supports_completions_request | ||
| 265 | .unwrap_or_default(); | ||
| 266 | |||
| 267 | if support_completions { | ||
| 268 | self.client_completions(&console, buffer, buffer_position, cx) | ||
| 269 | } else { | ||
| 270 | self.variable_list_completions(&console, buffer, buffer_position, cx) | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | fn resolve_completions( | ||
| 275 | &self, | ||
| 276 | _buffer: Entity<Buffer>, | ||
| 277 | _completion_indices: Vec<usize>, | ||
| 278 | _completions: Rc<RefCell<Box<[Completion]>>>, | ||
| 279 | _cx: &mut Context<Editor>, | ||
| 280 | ) -> gpui::Task<gpui::Result<bool>> { | ||
| 281 | Task::ready(Ok(false)) | ||
| 282 | } | ||
| 283 | |||
| 284 | fn apply_additional_edits_for_completion( | ||
| 285 | &self, | ||
| 286 | _buffer: Entity<Buffer>, | ||
| 287 | _completions: Rc<RefCell<Box<[Completion]>>>, | ||
| 288 | _completion_index: usize, | ||
| 289 | _push_to_history: bool, | ||
| 290 | _cx: &mut Context<Editor>, | ||
| 291 | ) -> gpui::Task<gpui::Result<Option<language::Transaction>>> { | ||
| 292 | Task::ready(Ok(None)) | ||
| 293 | } | ||
| 294 | |||
| 295 | fn is_completion_trigger( | ||
| 296 | &self, | ||
| 297 | _buffer: &Entity<Buffer>, | ||
| 298 | _position: language::Anchor, | ||
| 299 | _text: &str, | ||
| 300 | _trigger_in_words: bool, | ||
| 301 | _cx: &mut Context<Editor>, | ||
| 302 | ) -> bool { | ||
| 303 | true | ||
| 304 | } | ||
| 305 | } | ||
| 306 | |||
| 307 | impl ConsoleQueryBarCompletionProvider { | ||
| 308 | fn variable_list_completions( | ||
| 309 | &self, | ||
| 310 | console: &Entity<Console>, | ||
| 311 | buffer: &Entity<Buffer>, | ||
| 312 | buffer_position: language::Anchor, | ||
| 313 | cx: &mut Context<Editor>, | ||
| 314 | ) -> Task<Result<Option<Vec<Completion>>>> { | ||
| 315 | let (variables, string_matches) = console.update(cx, |console, cx| { | ||
| 316 | let mut variables = HashMap::default(); | ||
| 317 | let mut string_matches = Vec::default(); | ||
| 318 | |||
| 319 | for variable in console.variable_list.update(cx, |variable_list, cx| { | ||
| 320 | variable_list.completion_variables(cx) | ||
| 321 | }) { | ||
| 322 | if let Some(evaluate_name) = &variable.evaluate_name { | ||
| 323 | variables.insert(evaluate_name.clone(), variable.value.clone()); | ||
| 324 | string_matches.push(StringMatchCandidate { | ||
| 325 | id: 0, | ||
| 326 | string: evaluate_name.clone(), | ||
| 327 | char_bag: evaluate_name.chars().collect(), | ||
| 328 | }); | ||
| 329 | } | ||
| 330 | |||
| 331 | variables.insert(variable.name.clone(), variable.value.clone()); | ||
| 332 | |||
| 333 | string_matches.push(StringMatchCandidate { | ||
| 334 | id: 0, | ||
| 335 | string: variable.name.clone(), | ||
| 336 | char_bag: variable.name.chars().collect(), | ||
| 337 | }); | ||
| 338 | } | ||
| 339 | |||
| 340 | (variables, string_matches) | ||
| 341 | }); | ||
| 342 | |||
| 343 | let query = buffer.read(cx).text(); | ||
| 344 | |||
| 345 | cx.spawn(|_, cx| async move { | ||
| 346 | let matches = fuzzy::match_strings( | ||
| 347 | &string_matches, | ||
| 348 | &query, | ||
| 349 | true, | ||
| 350 | 10, | ||
| 351 | &Default::default(), | ||
| 352 | cx.background_executor().clone(), | ||
| 353 | ) | ||
| 354 | .await; | ||
| 355 | |||
| 356 | Ok(Some( | ||
| 357 | matches | ||
| 358 | .iter() | ||
| 359 | .filter_map(|string_match| { | ||
| 360 | let variable_value = variables.get(&string_match.string)?; | ||
| 361 | |||
| 362 | Some(project::Completion { | ||
| 363 | old_range: buffer_position..buffer_position, | ||
| 364 | new_text: string_match.string.clone(), | ||
| 365 | label: CodeLabel { | ||
| 366 | filter_range: 0..string_match.string.len(), | ||
| 367 | text: format!("{} {}", string_match.string.clone(), variable_value), | ||
| 368 | runs: Vec::new(), | ||
| 369 | }, | ||
| 370 | documentation: None, | ||
| 371 | confirm: None, | ||
| 372 | source: project::CompletionSource::Custom, | ||
| 373 | }) | ||
| 374 | }) | ||
| 375 | .collect(), | ||
| 376 | )) | ||
| 377 | }) | ||
| 378 | } | ||
| 379 | |||
| 380 | fn client_completions( | ||
| 381 | &self, | ||
| 382 | console: &Entity<Console>, | ||
| 383 | buffer: &Entity<Buffer>, | ||
| 384 | buffer_position: language::Anchor, | ||
| 385 | cx: &mut Context<Editor>, | ||
| 386 | ) -> Task<Result<Option<Vec<Completion>>>> { | ||
| 387 | let completion_task = console.update(cx, |console, cx| { | ||
| 388 | console.session.update(cx, |state, cx| { | ||
| 389 | let frame_id = console.stack_frame_list.read(cx).current_stack_frame_id(); | ||
| 390 | |||
| 391 | state.completions( | ||
| 392 | CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id), | ||
| 393 | cx, | ||
| 394 | ) | ||
| 395 | }) | ||
| 396 | }); | ||
| 397 | |||
| 398 | cx.background_executor().spawn(async move { | ||
| 399 | Ok(Some( | ||
| 400 | completion_task | ||
| 401 | .await? | ||
| 402 | .iter() | ||
| 403 | .map(|completion| project::Completion { | ||
| 404 | old_range: buffer_position..buffer_position, // TODO(debugger): change this | ||
| 405 | new_text: completion.text.clone().unwrap_or(completion.label.clone()), | ||
| 406 | label: CodeLabel { | ||
| 407 | filter_range: 0..completion.label.len(), | ||
| 408 | text: completion.label.clone(), | ||
| 409 | runs: Vec::new(), | ||
| 410 | }, | ||
| 411 | documentation: None, | ||
| 412 | confirm: None, | ||
| 413 | source: project::CompletionSource::Custom, | ||
| 414 | }) | ||
| 415 | .collect(), | ||
| 416 | )) | ||
| 417 | }) | ||
| 418 | } | ||
| 419 | } | ||
| @@ -0,0 +1,103 @@ | |||
| 1 | use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; | ||
| 2 | use project::debugger::session::{Session, SessionEvent}; | ||
| 3 | use ui::prelude::*; | ||
| 4 | use util::maybe; | ||
| 5 | |||
| 6 | pub struct LoadedSourceList { | ||
| 7 | list: ListState, | ||
| 8 | invalidate: bool, | ||
| 9 | focus_handle: FocusHandle, | ||
| 10 | _subscription: Subscription, | ||
| 11 | session: Entity<Session>, | ||
| 12 | } | ||
| 13 | |||
| 14 | impl LoadedSourceList { | ||
| 15 | pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self { | ||
| 16 | let weak_entity = cx.weak_entity(); | ||
| 17 | let focus_handle = cx.focus_handle(); | ||
| 18 | |||
| 19 | let list = ListState::new( | ||
| 20 | 0, | ||
| 21 | gpui::ListAlignment::Top, | ||
| 22 | px(1000.), | ||
| 23 | move |ix, _window, cx| { | ||
| 24 | weak_entity | ||
| 25 | .upgrade() | ||
| 26 | .map(|loaded_sources| { | ||
| 27 | loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx)) | ||
| 28 | }) | ||
| 29 | .unwrap_or(div().into_any()) | ||
| 30 | }, | ||
| 31 | ); | ||
| 32 | |||
| 33 | let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { | ||
| 34 | SessionEvent::Stopped(_) | SessionEvent::LoadedSources => { | ||
| 35 | this.invalidate = true; | ||
| 36 | cx.notify(); | ||
| 37 | } | ||
| 38 | _ => {} | ||
| 39 | }); | ||
| 40 | |||
| 41 | Self { | ||
| 42 | list, | ||
| 43 | session, | ||
| 44 | focus_handle, | ||
| 45 | _subscription, | ||
| 46 | invalidate: true, | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement { | ||
| 51 | let Some(source) = maybe!({ | ||
| 52 | self.session | ||
| 53 | .update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned()) | ||
| 54 | }) else { | ||
| 55 | return Empty.into_any(); | ||
| 56 | }; | ||
| 57 | |||
| 58 | v_flex() | ||
| 59 | .rounded_md() | ||
| 60 | .w_full() | ||
| 61 | .group("") | ||
| 62 | .p_1() | ||
| 63 | .hover(|s| s.bg(cx.theme().colors().element_hover)) | ||
| 64 | .child( | ||
| 65 | h_flex() | ||
| 66 | .gap_0p5() | ||
| 67 | .text_ui_sm(cx) | ||
| 68 | .when_some(source.name.clone(), |this, name| this.child(name)), | ||
| 69 | ) | ||
| 70 | .child( | ||
| 71 | h_flex() | ||
| 72 | .text_ui_xs(cx) | ||
| 73 | .text_color(cx.theme().colors().text_muted) | ||
| 74 | .when_some(source.path.clone(), |this, path| this.child(path)), | ||
| 75 | ) | ||
| 76 | .into_any() | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | impl Focusable for LoadedSourceList { | ||
| 81 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { | ||
| 82 | self.focus_handle.clone() | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | impl Render for LoadedSourceList { | ||
| 87 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 88 | if self.invalidate { | ||
| 89 | let len = self | ||
| 90 | .session | ||
| 91 | .update(cx, |session, cx| session.loaded_sources(cx).len()); | ||
| 92 | self.list.reset(len); | ||
| 93 | self.invalidate = false; | ||
| 94 | cx.notify(); | ||
| 95 | } | ||
| 96 | |||
| 97 | div() | ||
| 98 | .track_focus(&self.focus_handle) | ||
| 99 | .size_full() | ||
| 100 | .p_1() | ||
| 101 | .child(list(self.list.clone()).size_full()) | ||
| 102 | } | ||
| 103 | } | ||
| @@ -0,0 +1,183 @@ | |||
| 1 | use anyhow::anyhow; | ||
| 2 | use gpui::{ | ||
| 3 | list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, WeakEntity, | ||
| 4 | }; | ||
| 5 | use project::{ | ||
| 6 | debugger::session::{Session, SessionEvent}, | ||
| 7 | ProjectItem as _, ProjectPath, | ||
| 8 | }; | ||
| 9 | use std::{path::Path, sync::Arc}; | ||
| 10 | use ui::prelude::*; | ||
| 11 | use util::maybe; | ||
| 12 | use workspace::Workspace; | ||
| 13 | |||
| 14 | pub struct ModuleList { | ||
| 15 | list: ListState, | ||
| 16 | invalidate: bool, | ||
| 17 | session: Entity<Session>, | ||
| 18 | workspace: WeakEntity<Workspace>, | ||
| 19 | focus_handle: FocusHandle, | ||
| 20 | _subscription: Subscription, | ||
| 21 | } | ||
| 22 | |||
| 23 | impl ModuleList { | ||
| 24 | pub fn new( | ||
| 25 | session: Entity<Session>, | ||
| 26 | workspace: WeakEntity<Workspace>, | ||
| 27 | cx: &mut Context<Self>, | ||
| 28 | ) -> Self { | ||
| 29 | let weak_entity = cx.weak_entity(); | ||
| 30 | let focus_handle = cx.focus_handle(); | ||
| 31 | |||
| 32 | let list = ListState::new( | ||
| 33 | 0, | ||
| 34 | gpui::ListAlignment::Top, | ||
| 35 | px(1000.), | ||
| 36 | move |ix, _window, cx| { | ||
| 37 | weak_entity | ||
| 38 | .upgrade() | ||
| 39 | .map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx))) | ||
| 40 | .unwrap_or(div().into_any()) | ||
| 41 | }, | ||
| 42 | ); | ||
| 43 | |||
| 44 | let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { | ||
| 45 | SessionEvent::Stopped(_) | SessionEvent::Modules => { | ||
| 46 | this.invalidate = true; | ||
| 47 | cx.notify(); | ||
| 48 | } | ||
| 49 | _ => {} | ||
| 50 | }); | ||
| 51 | |||
| 52 | Self { | ||
| 53 | list, | ||
| 54 | session, | ||
| 55 | workspace, | ||
| 56 | focus_handle, | ||
| 57 | _subscription, | ||
| 58 | invalidate: true, | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) { | ||
| 63 | cx.spawn_in(window, move |this, mut cx| async move { | ||
| 64 | let (worktree, relative_path) = this | ||
| 65 | .update(&mut cx, |this, cx| { | ||
| 66 | this.workspace.update(cx, |workspace, cx| { | ||
| 67 | workspace.project().update(cx, |this, cx| { | ||
| 68 | this.find_or_create_worktree(&path, false, cx) | ||
| 69 | }) | ||
| 70 | }) | ||
| 71 | })?? | ||
| 72 | .await?; | ||
| 73 | |||
| 74 | let buffer = this | ||
| 75 | .update(&mut cx, |this, cx| { | ||
| 76 | this.workspace.update(cx, |this, cx| { | ||
| 77 | this.project().update(cx, |this, cx| { | ||
| 78 | let worktree_id = worktree.read(cx).id(); | ||
| 79 | this.open_buffer( | ||
| 80 | ProjectPath { | ||
| 81 | worktree_id, | ||
| 82 | path: relative_path.into(), | ||
| 83 | }, | ||
| 84 | cx, | ||
| 85 | ) | ||
| 86 | }) | ||
| 87 | }) | ||
| 88 | })?? | ||
| 89 | .await?; | ||
| 90 | |||
| 91 | this.update_in(&mut cx, |this, window, cx| { | ||
| 92 | this.workspace.update(cx, |workspace, cx| { | ||
| 93 | let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| { | ||
| 94 | anyhow!("Could not select a stack frame for unnamed buffer") | ||
| 95 | })?; | ||
| 96 | anyhow::Ok(workspace.open_path_preview( | ||
| 97 | project_path, | ||
| 98 | None, | ||
| 99 | false, | ||
| 100 | true, | ||
| 101 | true, | ||
| 102 | window, | ||
| 103 | cx, | ||
| 104 | )) | ||
| 105 | }) | ||
| 106 | })??? | ||
| 107 | .await?; | ||
| 108 | |||
| 109 | anyhow::Ok(()) | ||
| 110 | }) | ||
| 111 | .detach_and_log_err(cx); | ||
| 112 | } | ||
| 113 | |||
| 114 | fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement { | ||
| 115 | let Some(module) = maybe!({ | ||
| 116 | self.session | ||
| 117 | .update(cx, |state, cx| state.modules(cx).get(ix).cloned()) | ||
| 118 | }) else { | ||
| 119 | return Empty.into_any(); | ||
| 120 | }; | ||
| 121 | |||
| 122 | v_flex() | ||
| 123 | .rounded_md() | ||
| 124 | .w_full() | ||
| 125 | .group("") | ||
| 126 | .id(("module-list", ix)) | ||
| 127 | .when(module.path.is_some(), |this| { | ||
| 128 | this.on_click({ | ||
| 129 | let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path))); | ||
| 130 | cx.listener(move |this, _, window, cx| { | ||
| 131 | if let Some(path) = path.as_ref() { | ||
| 132 | this.open_module(path.clone(), window, cx); | ||
| 133 | } else { | ||
| 134 | log::error!("Wasn't able to find module path, but was still able to click on module list entry"); | ||
| 135 | } | ||
| 136 | }) | ||
| 137 | }) | ||
| 138 | }) | ||
| 139 | .p_1() | ||
| 140 | .hover(|s| s.bg(cx.theme().colors().element_hover)) | ||
| 141 | .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone())) | ||
| 142 | .child( | ||
| 143 | h_flex() | ||
| 144 | .text_ui_xs(cx) | ||
| 145 | .text_color(cx.theme().colors().text_muted) | ||
| 146 | .when_some(module.path.clone(), |this, path| this.child(path)), | ||
| 147 | ) | ||
| 148 | .into_any() | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | #[cfg(any(test, feature = "test-support"))] | ||
| 153 | impl ModuleList { | ||
| 154 | pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> { | ||
| 155 | self.session | ||
| 156 | .update(cx, |session, cx| session.modules(cx).to_vec()) | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | impl Focusable for ModuleList { | ||
| 161 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { | ||
| 162 | self.focus_handle.clone() | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | impl Render for ModuleList { | ||
| 167 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 168 | if self.invalidate { | ||
| 169 | let len = self | ||
| 170 | .session | ||
| 171 | .update(cx, |session, cx| session.modules(cx).len()); | ||
| 172 | self.list.reset(len); | ||
| 173 | self.invalidate = false; | ||
| 174 | cx.notify(); | ||
| 175 | } | ||
| 176 | |||
| 177 | div() | ||
| 178 | .track_focus(&self.focus_handle) | ||
| 179 | .size_full() | ||
| 180 | .p_1() | ||
| 181 | .child(list(self.list.clone()).size_full()) | ||
| 182 | } | ||
| 183 | } | ||
| @@ -0,0 +1,519 @@ | |||
| 1 | use std::path::Path; | ||
| 2 | use std::sync::Arc; | ||
| 3 | |||
| 4 | use anyhow::{anyhow, Result}; | ||
| 5 | use dap::StackFrameId; | ||
| 6 | use gpui::{ | ||
| 7 | list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, | ||
| 8 | WeakEntity, | ||
| 9 | }; | ||
| 10 | |||
| 11 | use language::PointUtf16; | ||
| 12 | use project::debugger::session::{Session, SessionEvent, StackFrame}; | ||
| 13 | use project::{ProjectItem, ProjectPath}; | ||
| 14 | use ui::{prelude::*, Tooltip}; | ||
| 15 | use util::ResultExt; | ||
| 16 | use workspace::Workspace; | ||
| 17 | |||
| 18 | use super::RunningState; | ||
| 19 | |||
| 20 | #[derive(Debug)] | ||
| 21 | pub enum StackFrameListEvent { | ||
| 22 | SelectedStackFrameChanged(StackFrameId), | ||
| 23 | } | ||
| 24 | |||
| 25 | pub struct StackFrameList { | ||
| 26 | list: ListState, | ||
| 27 | focus_handle: FocusHandle, | ||
| 28 | _subscription: Subscription, | ||
| 29 | session: Entity<Session>, | ||
| 30 | state: WeakEntity<RunningState>, | ||
| 31 | invalidate: bool, | ||
| 32 | entries: Vec<StackFrameEntry>, | ||
| 33 | workspace: WeakEntity<Workspace>, | ||
| 34 | current_stack_frame_id: Option<StackFrameId>, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[allow(clippy::large_enum_variant)] | ||
| 38 | #[derive(Debug, PartialEq, Eq)] | ||
| 39 | pub enum StackFrameEntry { | ||
| 40 | Normal(dap::StackFrame), | ||
| 41 | Collapsed(Vec<dap::StackFrame>), | ||
| 42 | } | ||
| 43 | |||
| 44 | impl StackFrameList { | ||
| 45 | pub fn new( | ||
| 46 | workspace: WeakEntity<Workspace>, | ||
| 47 | session: Entity<Session>, | ||
| 48 | state: WeakEntity<RunningState>, | ||
| 49 | window: &mut Window, | ||
| 50 | cx: &mut Context<Self>, | ||
| 51 | ) -> Self { | ||
| 52 | let weak_entity = cx.weak_entity(); | ||
| 53 | let focus_handle = cx.focus_handle(); | ||
| 54 | |||
| 55 | let list = ListState::new( | ||
| 56 | 0, | ||
| 57 | gpui::ListAlignment::Top, | ||
| 58 | px(1000.), | ||
| 59 | move |ix, _window, cx| { | ||
| 60 | weak_entity | ||
| 61 | .upgrade() | ||
| 62 | .map(|stack_frame_list| { | ||
| 63 | stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx)) | ||
| 64 | }) | ||
| 65 | .unwrap_or(div().into_any()) | ||
| 66 | }, | ||
| 67 | ); | ||
| 68 | |||
| 69 | let _subscription = | ||
| 70 | cx.subscribe_in(&session, window, |this, _, event, _, cx| match event { | ||
| 71 | SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => { | ||
| 72 | this.refresh(cx); | ||
| 73 | } | ||
| 74 | _ => {} | ||
| 75 | }); | ||
| 76 | |||
| 77 | Self { | ||
| 78 | list, | ||
| 79 | session, | ||
| 80 | workspace, | ||
| 81 | focus_handle, | ||
| 82 | state, | ||
| 83 | _subscription, | ||
| 84 | invalidate: true, | ||
| 85 | entries: Default::default(), | ||
| 86 | current_stack_frame_id: None, | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | #[cfg(any(test, feature = "test-support"))] | ||
| 91 | pub fn entries(&self) -> &Vec<StackFrameEntry> { | ||
| 92 | &self.entries | ||
| 93 | } | ||
| 94 | |||
| 95 | #[cfg(any(test, feature = "test-support"))] | ||
| 96 | pub fn flatten_entries(&self) -> Vec<dap::StackFrame> { | ||
| 97 | self.entries | ||
| 98 | .iter() | ||
| 99 | .flat_map(|frame| match frame { | ||
| 100 | StackFrameEntry::Normal(frame) => vec![frame.clone()], | ||
| 101 | StackFrameEntry::Collapsed(frames) => frames.clone(), | ||
| 102 | }) | ||
| 103 | .collect::<Vec<_>>() | ||
| 104 | } | ||
| 105 | |||
| 106 | fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> { | ||
| 107 | self.state | ||
| 108 | .read_with(cx, |state, _| state.thread_id) | ||
| 109 | .log_err() | ||
| 110 | .flatten() | ||
| 111 | .map(|thread_id| { | ||
| 112 | self.session | ||
| 113 | .update(cx, |this, cx| this.stack_frames(thread_id, cx)) | ||
| 114 | }) | ||
| 115 | .unwrap_or_default() | ||
| 116 | } | ||
| 117 | |||
| 118 | #[cfg(any(test, feature = "test-support"))] | ||
| 119 | pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> { | ||
| 120 | self.stack_frames(cx) | ||
| 121 | .into_iter() | ||
| 122 | .map(|stack_frame| stack_frame.dap.clone()) | ||
| 123 | .collect() | ||
| 124 | } | ||
| 125 | |||
| 126 | pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 { | ||
| 127 | self.stack_frames(cx) | ||
| 128 | .first() | ||
| 129 | .map(|stack_frame| stack_frame.dap.id) | ||
| 130 | .unwrap_or(0) | ||
| 131 | } | ||
| 132 | |||
| 133 | pub fn current_stack_frame_id(&self) -> Option<StackFrameId> { | ||
| 134 | self.current_stack_frame_id | ||
| 135 | } | ||
| 136 | |||
| 137 | pub(super) fn refresh(&mut self, cx: &mut Context<Self>) { | ||
| 138 | self.invalidate = true; | ||
| 139 | self.entries.clear(); | ||
| 140 | cx.notify(); | ||
| 141 | } | ||
| 142 | |||
| 143 | pub fn build_entries( | ||
| 144 | &mut self, | ||
| 145 | select_first_stack_frame: bool, | ||
| 146 | window: &mut Window, | ||
| 147 | cx: &mut Context<Self>, | ||
| 148 | ) { | ||
| 149 | let mut entries = Vec::new(); | ||
| 150 | let mut collapsed_entries = Vec::new(); | ||
| 151 | let mut current_stack_frame = None; | ||
| 152 | |||
| 153 | let stack_frames = self.stack_frames(cx); | ||
| 154 | for stack_frame in &stack_frames { | ||
| 155 | match stack_frame.dap.presentation_hint { | ||
| 156 | Some(dap::StackFramePresentationHint::Deemphasize) => { | ||
| 157 | collapsed_entries.push(stack_frame.dap.clone()); | ||
| 158 | } | ||
| 159 | _ => { | ||
| 160 | let collapsed_entries = std::mem::take(&mut collapsed_entries); | ||
| 161 | if !collapsed_entries.is_empty() { | ||
| 162 | entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); | ||
| 163 | } | ||
| 164 | |||
| 165 | current_stack_frame.get_or_insert(&stack_frame.dap); | ||
| 166 | entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | let collapsed_entries = std::mem::take(&mut collapsed_entries); | ||
| 172 | if !collapsed_entries.is_empty() { | ||
| 173 | entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); | ||
| 174 | } | ||
| 175 | |||
| 176 | std::mem::swap(&mut self.entries, &mut entries); | ||
| 177 | self.list.reset(self.entries.len()); | ||
| 178 | |||
| 179 | if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame) | ||
| 180 | { | ||
| 181 | self.select_stack_frame(current_stack_frame, true, window, cx) | ||
| 182 | .detach_and_log_err(cx); | ||
| 183 | } | ||
| 184 | |||
| 185 | cx.notify(); | ||
| 186 | } | ||
| 187 | |||
| 188 | pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) { | ||
| 189 | if let Some(current_stack_frame_id) = self.current_stack_frame_id { | ||
| 190 | let frame = self | ||
| 191 | .entries | ||
| 192 | .iter() | ||
| 193 | .find_map(|entry| match entry { | ||
| 194 | StackFrameEntry::Normal(dap) => { | ||
| 195 | if dap.id == current_stack_frame_id { | ||
| 196 | Some(dap) | ||
| 197 | } else { | ||
| 198 | None | ||
| 199 | } | ||
| 200 | } | ||
| 201 | StackFrameEntry::Collapsed(daps) => { | ||
| 202 | daps.iter().find(|dap| dap.id == current_stack_frame_id) | ||
| 203 | } | ||
| 204 | }) | ||
| 205 | .cloned(); | ||
| 206 | |||
| 207 | if let Some(frame) = frame.as_ref() { | ||
| 208 | self.select_stack_frame(frame, true, window, cx) | ||
| 209 | .detach_and_log_err(cx); | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | pub fn select_stack_frame( | ||
| 215 | &mut self, | ||
| 216 | stack_frame: &dap::StackFrame, | ||
| 217 | go_to_stack_frame: bool, | ||
| 218 | window: &Window, | ||
| 219 | cx: &mut Context<Self>, | ||
| 220 | ) -> Task<Result<()>> { | ||
| 221 | self.current_stack_frame_id = Some(stack_frame.id); | ||
| 222 | |||
| 223 | cx.emit(StackFrameListEvent::SelectedStackFrameChanged( | ||
| 224 | stack_frame.id, | ||
| 225 | )); | ||
| 226 | cx.notify(); | ||
| 227 | |||
| 228 | if !go_to_stack_frame { | ||
| 229 | return Task::ready(Ok(())); | ||
| 230 | }; | ||
| 231 | |||
| 232 | let row = (stack_frame.line.saturating_sub(1)) as u32; | ||
| 233 | |||
| 234 | let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else { | ||
| 235 | return Task::ready(Err(anyhow!("Project path not found"))); | ||
| 236 | }; | ||
| 237 | |||
| 238 | cx.spawn_in(window, move |this, mut cx| async move { | ||
| 239 | let (worktree, relative_path) = this | ||
| 240 | .update(&mut cx, |this, cx| { | ||
| 241 | this.workspace.update(cx, |workspace, cx| { | ||
| 242 | workspace.project().update(cx, |this, cx| { | ||
| 243 | this.find_or_create_worktree(&abs_path, false, cx) | ||
| 244 | }) | ||
| 245 | }) | ||
| 246 | })?? | ||
| 247 | .await?; | ||
| 248 | let buffer = this | ||
| 249 | .update(&mut cx, |this, cx| { | ||
| 250 | this.workspace.update(cx, |this, cx| { | ||
| 251 | this.project().update(cx, |this, cx| { | ||
| 252 | let worktree_id = worktree.read(cx).id(); | ||
| 253 | this.open_buffer( | ||
| 254 | ProjectPath { | ||
| 255 | worktree_id, | ||
| 256 | path: relative_path.into(), | ||
| 257 | }, | ||
| 258 | cx, | ||
| 259 | ) | ||
| 260 | }) | ||
| 261 | }) | ||
| 262 | })?? | ||
| 263 | .await?; | ||
| 264 | let position = buffer.update(&mut cx, |this, _| { | ||
| 265 | this.snapshot().anchor_after(PointUtf16::new(row, 0)) | ||
| 266 | })?; | ||
| 267 | this.update_in(&mut cx, |this, window, cx| { | ||
| 268 | this.workspace.update(cx, |workspace, cx| { | ||
| 269 | let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| { | ||
| 270 | anyhow!("Could not select a stack frame for unnamed buffer") | ||
| 271 | })?; | ||
| 272 | anyhow::Ok(workspace.open_path_preview( | ||
| 273 | project_path, | ||
| 274 | None, | ||
| 275 | false, | ||
| 276 | true, | ||
| 277 | true, | ||
| 278 | window, | ||
| 279 | cx, | ||
| 280 | )) | ||
| 281 | }) | ||
| 282 | })??? | ||
| 283 | .await?; | ||
| 284 | |||
| 285 | this.update(&mut cx, |this, cx| { | ||
| 286 | this.workspace.update(cx, |workspace, cx| { | ||
| 287 | let breakpoint_store = workspace.project().read(cx).breakpoint_store(); | ||
| 288 | |||
| 289 | breakpoint_store.update(cx, |store, cx| { | ||
| 290 | store.set_active_position( | ||
| 291 | (this.session.read(cx).session_id(), abs_path, position), | ||
| 292 | cx, | ||
| 293 | ); | ||
| 294 | }) | ||
| 295 | }) | ||
| 296 | })? | ||
| 297 | }) | ||
| 298 | } | ||
| 299 | |||
| 300 | fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> { | ||
| 301 | stack_frame.source.as_ref().and_then(|s| { | ||
| 302 | s.path | ||
| 303 | .as_deref() | ||
| 304 | .map(|path| Arc::<Path>::from(Path::new(path))) | ||
| 305 | }) | ||
| 306 | } | ||
| 307 | |||
| 308 | pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) { | ||
| 309 | self.session.update(cx, |state, cx| { | ||
| 310 | state.restart_stack_frame(stack_frame_id, cx) | ||
| 311 | }); | ||
| 312 | } | ||
| 313 | |||
| 314 | fn render_normal_entry( | ||
| 315 | &self, | ||
| 316 | stack_frame: &dap::StackFrame, | ||
| 317 | cx: &mut Context<Self>, | ||
| 318 | ) -> AnyElement { | ||
| 319 | let source = stack_frame.source.clone(); | ||
| 320 | let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id; | ||
| 321 | |||
| 322 | let formatted_path = format!( | ||
| 323 | "{}:{}", | ||
| 324 | source.clone().and_then(|s| s.name).unwrap_or_default(), | ||
| 325 | stack_frame.line, | ||
| 326 | ); | ||
| 327 | |||
| 328 | let supports_frame_restart = self | ||
| 329 | .session | ||
| 330 | .read(cx) | ||
| 331 | .capabilities() | ||
| 332 | .supports_restart_frame | ||
| 333 | .unwrap_or_default(); | ||
| 334 | |||
| 335 | let origin = stack_frame | ||
| 336 | .source | ||
| 337 | .to_owned() | ||
| 338 | .and_then(|source| source.origin); | ||
| 339 | |||
| 340 | h_flex() | ||
| 341 | .rounded_md() | ||
| 342 | .justify_between() | ||
| 343 | .w_full() | ||
| 344 | .group("") | ||
| 345 | .id(("stack-frame", stack_frame.id)) | ||
| 346 | .tooltip({ | ||
| 347 | let formatted_path = formatted_path.clone(); | ||
| 348 | move |_window, app| { | ||
| 349 | app.new(|_| { | ||
| 350 | let mut tooltip = Tooltip::new(formatted_path.clone()); | ||
| 351 | |||
| 352 | if let Some(origin) = &origin { | ||
| 353 | tooltip = tooltip.meta(origin); | ||
| 354 | } | ||
| 355 | |||
| 356 | tooltip | ||
| 357 | }) | ||
| 358 | .into() | ||
| 359 | } | ||
| 360 | }) | ||
| 361 | .p_1() | ||
| 362 | .when(is_selected_frame, |this| { | ||
| 363 | this.bg(cx.theme().colors().element_hover) | ||
| 364 | }) | ||
| 365 | .on_click(cx.listener({ | ||
| 366 | let stack_frame = stack_frame.clone(); | ||
| 367 | move |this, _, window, cx| { | ||
| 368 | this.select_stack_frame(&stack_frame, true, window, cx) | ||
| 369 | .detach_and_log_err(cx); | ||
| 370 | } | ||
| 371 | })) | ||
| 372 | .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer()) | ||
| 373 | .child( | ||
| 374 | v_flex() | ||
| 375 | .child( | ||
| 376 | h_flex() | ||
| 377 | .gap_0p5() | ||
| 378 | .text_ui_sm(cx) | ||
| 379 | .truncate() | ||
| 380 | .child(stack_frame.name.clone()) | ||
| 381 | .child(formatted_path), | ||
| 382 | ) | ||
| 383 | .child( | ||
| 384 | h_flex() | ||
| 385 | .text_ui_xs(cx) | ||
| 386 | .truncate() | ||
| 387 | .text_color(cx.theme().colors().text_muted) | ||
| 388 | .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), | ||
| 389 | ), | ||
| 390 | ) | ||
| 391 | .when( | ||
| 392 | supports_frame_restart && stack_frame.can_restart.unwrap_or(true), | ||
| 393 | |this| { | ||
| 394 | this.child( | ||
| 395 | h_flex() | ||
| 396 | .id(("restart-stack-frame", stack_frame.id)) | ||
| 397 | .visible_on_hover("") | ||
| 398 | .absolute() | ||
| 399 | .right_2() | ||
| 400 | .overflow_hidden() | ||
| 401 | .rounded_md() | ||
| 402 | .border_1() | ||
| 403 | .border_color(cx.theme().colors().element_selected) | ||
| 404 | .bg(cx.theme().colors().element_background) | ||
| 405 | .hover(|style| { | ||
| 406 | style | ||
| 407 | .bg(cx.theme().colors().ghost_element_hover) | ||
| 408 | .cursor_pointer() | ||
| 409 | }) | ||
| 410 | .child( | ||
| 411 | IconButton::new( | ||
| 412 | ("restart-stack-frame", stack_frame.id), | ||
| 413 | IconName::DebugRestart, | ||
| 414 | ) | ||
| 415 | .icon_size(IconSize::Small) | ||
| 416 | .on_click(cx.listener({ | ||
| 417 | let stack_frame_id = stack_frame.id; | ||
| 418 | move |this, _, _window, cx| { | ||
| 419 | this.restart_stack_frame(stack_frame_id, cx); | ||
| 420 | } | ||
| 421 | })) | ||
| 422 | .tooltip(move |window, cx| { | ||
| 423 | Tooltip::text("Restart Stack Frame")(window, cx) | ||
| 424 | }), | ||
| 425 | ), | ||
| 426 | ) | ||
| 427 | }, | ||
| 428 | ) | ||
| 429 | .into_any() | ||
| 430 | } | ||
| 431 | |||
| 432 | pub fn expand_collapsed_entry( | ||
| 433 | &mut self, | ||
| 434 | ix: usize, | ||
| 435 | stack_frames: &Vec<dap::StackFrame>, | ||
| 436 | cx: &mut Context<Self>, | ||
| 437 | ) { | ||
| 438 | self.entries.splice( | ||
| 439 | ix..ix + 1, | ||
| 440 | stack_frames | ||
| 441 | .iter() | ||
| 442 | .map(|frame| StackFrameEntry::Normal(frame.clone())), | ||
| 443 | ); | ||
| 444 | self.list.reset(self.entries.len()); | ||
| 445 | cx.notify(); | ||
| 446 | } | ||
| 447 | |||
| 448 | fn render_collapsed_entry( | ||
| 449 | &self, | ||
| 450 | ix: usize, | ||
| 451 | stack_frames: &Vec<dap::StackFrame>, | ||
| 452 | cx: &mut Context<Self>, | ||
| 453 | ) -> AnyElement { | ||
| 454 | let first_stack_frame = &stack_frames[0]; | ||
| 455 | |||
| 456 | h_flex() | ||
| 457 | .rounded_md() | ||
| 458 | .justify_between() | ||
| 459 | .w_full() | ||
| 460 | .group("") | ||
| 461 | .id(("stack-frame", first_stack_frame.id)) | ||
| 462 | .p_1() | ||
| 463 | .on_click(cx.listener({ | ||
| 464 | let stack_frames = stack_frames.clone(); | ||
| 465 | move |this, _, _window, cx| { | ||
| 466 | this.expand_collapsed_entry(ix, &stack_frames, cx); | ||
| 467 | } | ||
| 468 | })) | ||
| 469 | .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer()) | ||
| 470 | .child( | ||
| 471 | v_flex() | ||
| 472 | .text_ui_sm(cx) | ||
| 473 | .truncate() | ||
| 474 | .text_color(cx.theme().colors().text_muted) | ||
| 475 | .child(format!( | ||
| 476 | "Show {} more{}", | ||
| 477 | stack_frames.len(), | ||
| 478 | first_stack_frame | ||
| 479 | .source | ||
| 480 | .as_ref() | ||
| 481 | .and_then(|source| source.origin.as_ref()) | ||
| 482 | .map_or(String::new(), |origin| format!(": {}", origin)) | ||
| 483 | )), | ||
| 484 | ) | ||
| 485 | .into_any() | ||
| 486 | } | ||
| 487 | |||
| 488 | fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement { | ||
| 489 | match &self.entries[ix] { | ||
| 490 | StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx), | ||
| 491 | StackFrameEntry::Collapsed(stack_frames) => { | ||
| 492 | self.render_collapsed_entry(ix, stack_frames, cx) | ||
| 493 | } | ||
| 494 | } | ||
| 495 | } | ||
| 496 | } | ||
| 497 | |||
| 498 | impl Render for StackFrameList { | ||
| 499 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 500 | if self.invalidate { | ||
| 501 | self.build_entries(self.entries.is_empty(), window, cx); | ||
| 502 | self.invalidate = false; | ||
| 503 | cx.notify(); | ||
| 504 | } | ||
| 505 | |||
| 506 | div() | ||
| 507 | .size_full() | ||
| 508 | .p_1() | ||
| 509 | .child(list(self.list.clone()).size_full()) | ||
| 510 | } | ||
| 511 | } | ||
| 512 | |||
| 513 | impl Focusable for StackFrameList { | ||
| 514 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { | ||
| 515 | self.focus_handle.clone() | ||
| 516 | } | ||
| 517 | } | ||
| 518 | |||
| 519 | impl EventEmitter<StackFrameListEvent> for StackFrameList {} | ||
| @@ -0,0 +1,946 @@ | |||
| 1 | use super::stack_frame_list::{StackFrameList, StackFrameListEvent}; | ||
| 2 | use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference}; | ||
| 3 | use editor::Editor; | ||
| 4 | use gpui::{ | ||
| 5 | actions, anchored, deferred, uniform_list, AnyElement, ClickEvent, ClipboardItem, Context, | ||
| 6 | DismissEvent, Entity, FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, | ||
| 7 | Stateful, Subscription, TextStyleRefinement, UniformListScrollHandle, | ||
| 8 | }; | ||
| 9 | use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; | ||
| 10 | use project::debugger::session::{Session, SessionEvent}; | ||
| 11 | use std::{collections::HashMap, ops::Range, sync::Arc}; | ||
| 12 | use ui::{prelude::*, ContextMenu, ListItem, Scrollbar, ScrollbarState}; | ||
| 13 | use util::{debug_panic, maybe}; | ||
| 14 | |||
| 15 | actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]); | ||
| 16 | |||
| 17 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] | ||
| 18 | pub(crate) struct EntryState { | ||
| 19 | depth: usize, | ||
| 20 | is_expanded: bool, | ||
| 21 | parent_reference: VariableReference, | ||
| 22 | } | ||
| 23 | |||
| 24 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] | ||
| 25 | pub(crate) struct EntryPath { | ||
| 26 | pub leaf_name: Option<SharedString>, | ||
| 27 | pub indices: Arc<[SharedString]>, | ||
| 28 | } | ||
| 29 | |||
| 30 | impl EntryPath { | ||
| 31 | fn for_scope(scope_name: impl Into<SharedString>) -> Self { | ||
| 32 | Self { | ||
| 33 | leaf_name: Some(scope_name.into()), | ||
| 34 | indices: Arc::new([]), | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | fn with_name(&self, name: SharedString) -> Self { | ||
| 39 | Self { | ||
| 40 | leaf_name: Some(name), | ||
| 41 | indices: self.indices.clone(), | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | /// Create a new child of this variable path | ||
| 46 | fn with_child(&self, name: SharedString) -> Self { | ||
| 47 | Self { | ||
| 48 | leaf_name: None, | ||
| 49 | indices: self | ||
| 50 | .indices | ||
| 51 | .iter() | ||
| 52 | .cloned() | ||
| 53 | .chain(std::iter::once(name)) | ||
| 54 | .collect(), | ||
| 55 | } | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | #[derive(Debug, Clone, PartialEq)] | ||
| 60 | enum EntryKind { | ||
| 61 | Variable(dap::Variable), | ||
| 62 | Scope(dap::Scope), | ||
| 63 | } | ||
| 64 | |||
| 65 | impl EntryKind { | ||
| 66 | fn as_variable(&self) -> Option<&dap::Variable> { | ||
| 67 | match self { | ||
| 68 | EntryKind::Variable(dap) => Some(dap), | ||
| 69 | _ => None, | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | fn as_scope(&self) -> Option<&dap::Scope> { | ||
| 74 | match self { | ||
| 75 | EntryKind::Scope(dap) => Some(dap), | ||
| 76 | _ => None, | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | #[allow(dead_code)] | ||
| 81 | fn name(&self) -> &str { | ||
| 82 | match self { | ||
| 83 | EntryKind::Variable(dap) => &dap.name, | ||
| 84 | EntryKind::Scope(dap) => &dap.name, | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | #[derive(Debug, Clone, PartialEq)] | ||
| 90 | struct ListEntry { | ||
| 91 | dap_kind: EntryKind, | ||
| 92 | path: EntryPath, | ||
| 93 | } | ||
| 94 | |||
| 95 | impl ListEntry { | ||
| 96 | fn as_variable(&self) -> Option<&dap::Variable> { | ||
| 97 | self.dap_kind.as_variable() | ||
| 98 | } | ||
| 99 | |||
| 100 | fn as_scope(&self) -> Option<&dap::Scope> { | ||
| 101 | self.dap_kind.as_scope() | ||
| 102 | } | ||
| 103 | |||
| 104 | fn item_id(&self) -> ElementId { | ||
| 105 | use std::fmt::Write; | ||
| 106 | let mut id = match &self.dap_kind { | ||
| 107 | EntryKind::Variable(dap) => format!("variable-{}", dap.name), | ||
| 108 | EntryKind::Scope(dap) => format!("scope-{}", dap.name), | ||
| 109 | }; | ||
| 110 | for name in self.path.indices.iter() { | ||
| 111 | _ = write!(id, "-{}", name); | ||
| 112 | } | ||
| 113 | SharedString::from(id).into() | ||
| 114 | } | ||
| 115 | |||
| 116 | fn item_value_id(&self) -> ElementId { | ||
| 117 | use std::fmt::Write; | ||
| 118 | let mut id = match &self.dap_kind { | ||
| 119 | EntryKind::Variable(dap) => format!("variable-{}", dap.name), | ||
| 120 | EntryKind::Scope(dap) => format!("scope-{}", dap.name), | ||
| 121 | }; | ||
| 122 | for name in self.path.indices.iter() { | ||
| 123 | _ = write!(id, "-{}", name); | ||
| 124 | } | ||
| 125 | _ = write!(id, "-value"); | ||
| 126 | SharedString::from(id).into() | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | pub struct VariableList { | ||
| 131 | entries: Vec<ListEntry>, | ||
| 132 | entry_states: HashMap<EntryPath, EntryState>, | ||
| 133 | selected_stack_frame_id: Option<StackFrameId>, | ||
| 134 | list_handle: UniformListScrollHandle, | ||
| 135 | scrollbar_state: ScrollbarState, | ||
| 136 | session: Entity<Session>, | ||
| 137 | selection: Option<EntryPath>, | ||
| 138 | open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, | ||
| 139 | focus_handle: FocusHandle, | ||
| 140 | edited_path: Option<(EntryPath, Entity<Editor>)>, | ||
| 141 | disabled: bool, | ||
| 142 | _subscriptions: Vec<Subscription>, | ||
| 143 | } | ||
| 144 | |||
| 145 | impl VariableList { | ||
| 146 | pub fn new( | ||
| 147 | session: Entity<Session>, | ||
| 148 | stack_frame_list: Entity<StackFrameList>, | ||
| 149 | window: &mut Window, | ||
| 150 | cx: &mut Context<Self>, | ||
| 151 | ) -> Self { | ||
| 152 | let focus_handle = cx.focus_handle(); | ||
| 153 | |||
| 154 | let _subscriptions = vec![ | ||
| 155 | cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events), | ||
| 156 | cx.subscribe(&session, |this, _, event, _| match event { | ||
| 157 | SessionEvent::Stopped(_) => { | ||
| 158 | this.selection.take(); | ||
| 159 | this.edited_path.take(); | ||
| 160 | this.selected_stack_frame_id.take(); | ||
| 161 | } | ||
| 162 | _ => {} | ||
| 163 | }), | ||
| 164 | cx.on_focus_out(&focus_handle, window, |this, _, _, cx| { | ||
| 165 | this.edited_path.take(); | ||
| 166 | cx.notify(); | ||
| 167 | }), | ||
| 168 | ]; | ||
| 169 | |||
| 170 | let list_state = UniformListScrollHandle::default(); | ||
| 171 | |||
| 172 | Self { | ||
| 173 | scrollbar_state: ScrollbarState::new(list_state.clone()), | ||
| 174 | list_handle: list_state, | ||
| 175 | session, | ||
| 176 | focus_handle, | ||
| 177 | _subscriptions, | ||
| 178 | selected_stack_frame_id: None, | ||
| 179 | selection: None, | ||
| 180 | open_context_menu: None, | ||
| 181 | disabled: false, | ||
| 182 | edited_path: None, | ||
| 183 | entries: Default::default(), | ||
| 184 | entry_states: Default::default(), | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) { | ||
| 189 | let old_disabled = std::mem::take(&mut self.disabled); | ||
| 190 | self.disabled = disabled; | ||
| 191 | if old_disabled != disabled { | ||
| 192 | cx.notify(); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | fn build_entries(&mut self, cx: &mut Context<Self>) { | ||
| 197 | let Some(stack_frame_id) = self.selected_stack_frame_id else { | ||
| 198 | return; | ||
| 199 | }; | ||
| 200 | |||
| 201 | let mut entries = vec![]; | ||
| 202 | let scopes: Vec<_> = self.session.update(cx, |session, cx| { | ||
| 203 | session.scopes(stack_frame_id, cx).iter().cloned().collect() | ||
| 204 | }); | ||
| 205 | |||
| 206 | let mut contains_local_scope = false; | ||
| 207 | |||
| 208 | let mut stack = scopes | ||
| 209 | .into_iter() | ||
| 210 | .rev() | ||
| 211 | .filter(|scope| { | ||
| 212 | if scope | ||
| 213 | .presentation_hint | ||
| 214 | .as_ref() | ||
| 215 | .map(|hint| *hint == ScopePresentationHint::Locals) | ||
| 216 | .unwrap_or(scope.name.to_lowercase().starts_with("local")) | ||
| 217 | { | ||
| 218 | contains_local_scope = true; | ||
| 219 | } | ||
| 220 | |||
| 221 | self.session.update(cx, |session, cx| { | ||
| 222 | session.variables(scope.variables_reference, cx).len() > 0 | ||
| 223 | }) | ||
| 224 | }) | ||
| 225 | .map(|scope| { | ||
| 226 | ( | ||
| 227 | scope.variables_reference, | ||
| 228 | scope.variables_reference, | ||
| 229 | EntryPath::for_scope(&scope.name), | ||
| 230 | EntryKind::Scope(scope), | ||
| 231 | ) | ||
| 232 | }) | ||
| 233 | .collect::<Vec<_>>(); | ||
| 234 | |||
| 235 | let scopes_count = stack.len(); | ||
| 236 | |||
| 237 | while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop() | ||
| 238 | { | ||
| 239 | match &dap_kind { | ||
| 240 | EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()), | ||
| 241 | EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()), | ||
| 242 | } | ||
| 243 | |||
| 244 | let var_state = self | ||
| 245 | .entry_states | ||
| 246 | .entry(path.clone()) | ||
| 247 | .and_modify(|state| { | ||
| 248 | state.parent_reference = container_reference; | ||
| 249 | }) | ||
| 250 | .or_insert(EntryState { | ||
| 251 | depth: path.indices.len(), | ||
| 252 | is_expanded: dap_kind.as_scope().is_some_and(|scope| { | ||
| 253 | (scopes_count == 1 && !contains_local_scope) | ||
| 254 | || scope | ||
| 255 | .presentation_hint | ||
| 256 | .as_ref() | ||
| 257 | .map(|hint| *hint == ScopePresentationHint::Locals) | ||
| 258 | .unwrap_or(scope.name.to_lowercase().starts_with("local")) | ||
| 259 | }), | ||
| 260 | parent_reference: container_reference, | ||
| 261 | }); | ||
| 262 | |||
| 263 | entries.push(ListEntry { | ||
| 264 | dap_kind, | ||
| 265 | path: path.clone(), | ||
| 266 | }); | ||
| 267 | |||
| 268 | if var_state.is_expanded { | ||
| 269 | let children = self | ||
| 270 | .session | ||
| 271 | .update(cx, |session, cx| session.variables(variables_reference, cx)); | ||
| 272 | stack.extend(children.into_iter().rev().map(|child| { | ||
| 273 | ( | ||
| 274 | variables_reference, | ||
| 275 | child.variables_reference, | ||
| 276 | path.with_child(child.name.clone().into()), | ||
| 277 | EntryKind::Variable(child), | ||
| 278 | ) | ||
| 279 | })); | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | self.entries = entries; | ||
| 284 | cx.notify(); | ||
| 285 | } | ||
| 286 | |||
| 287 | fn handle_stack_frame_list_events( | ||
| 288 | &mut self, | ||
| 289 | _: Entity<StackFrameList>, | ||
| 290 | event: &StackFrameListEvent, | ||
| 291 | cx: &mut Context<Self>, | ||
| 292 | ) { | ||
| 293 | match event { | ||
| 294 | StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => { | ||
| 295 | self.selected_stack_frame_id = Some(*stack_frame_id); | ||
| 296 | cx.notify(); | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } | ||
| 300 | |||
| 301 | pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> { | ||
| 302 | self.entries | ||
| 303 | .iter() | ||
| 304 | .filter_map(|entry| match &entry.dap_kind { | ||
| 305 | EntryKind::Variable(dap) => Some(dap.clone()), | ||
| 306 | EntryKind::Scope(_) => None, | ||
| 307 | }) | ||
| 308 | .collect() | ||
| 309 | } | ||
| 310 | |||
| 311 | fn render_entries( | ||
| 312 | &mut self, | ||
| 313 | ix: Range<usize>, | ||
| 314 | window: &mut Window, | ||
| 315 | cx: &mut Context<Self>, | ||
| 316 | ) -> Vec<AnyElement> { | ||
| 317 | ix.into_iter() | ||
| 318 | .filter_map(|ix| { | ||
| 319 | let (entry, state) = self | ||
| 320 | .entries | ||
| 321 | .get(ix) | ||
| 322 | .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?; | ||
| 323 | |||
| 324 | match &entry.dap_kind { | ||
| 325 | EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)), | ||
| 326 | EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)), | ||
| 327 | } | ||
| 328 | }) | ||
| 329 | .collect() | ||
| 330 | } | ||
| 331 | |||
| 332 | pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) { | ||
| 333 | let Some(entry) = self.entry_states.get_mut(var_path) else { | ||
| 334 | log::error!("Could not find variable list entry state to toggle"); | ||
| 335 | return; | ||
| 336 | }; | ||
| 337 | |||
| 338 | entry.is_expanded = !entry.is_expanded; | ||
| 339 | cx.notify(); | ||
| 340 | } | ||
| 341 | |||
| 342 | fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) { | ||
| 343 | self.cancel_variable_edit(&Default::default(), window, cx); | ||
| 344 | if let Some(variable) = self.entries.first() { | ||
| 345 | self.selection = Some(variable.path.clone()); | ||
| 346 | cx.notify(); | ||
| 347 | } | ||
| 348 | } | ||
| 349 | |||
| 350 | fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) { | ||
| 351 | self.cancel_variable_edit(&Default::default(), window, cx); | ||
| 352 | if let Some(variable) = self.entries.last() { | ||
| 353 | self.selection = Some(variable.path.clone()); | ||
| 354 | cx.notify(); | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) { | ||
| 359 | self.cancel_variable_edit(&Default::default(), window, cx); | ||
| 360 | if let Some(selection) = &self.selection { | ||
| 361 | if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| { | ||
| 362 | if &var.path == selection { | ||
| 363 | Some(ix.saturating_sub(1)) | ||
| 364 | } else { | ||
| 365 | None | ||
| 366 | } | ||
| 367 | }) { | ||
| 368 | if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) { | ||
| 369 | self.selection = Some(new_selection); | ||
| 370 | cx.notify(); | ||
| 371 | } else { | ||
| 372 | self.select_first(&SelectFirst, window, cx); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } else { | ||
| 376 | self.select_first(&SelectFirst, window, cx); | ||
| 377 | } | ||
| 378 | } | ||
| 379 | |||
| 380 | fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) { | ||
| 381 | self.cancel_variable_edit(&Default::default(), window, cx); | ||
| 382 | if let Some(selection) = &self.selection { | ||
| 383 | if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| { | ||
| 384 | if &var.path == selection { | ||
| 385 | Some(ix.saturating_add(1)) | ||
| 386 | } else { | ||
| 387 | None | ||
| 388 | } | ||
| 389 | }) { | ||
| 390 | if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) { | ||
| 391 | self.selection = Some(new_selection); | ||
| 392 | cx.notify(); | ||
| 393 | } else { | ||
| 394 | self.select_first(&SelectFirst, window, cx); | ||
| 395 | } | ||
| 396 | } | ||
| 397 | } else { | ||
| 398 | self.select_first(&SelectFirst, window, cx); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | fn cancel_variable_edit( | ||
| 403 | &mut self, | ||
| 404 | _: &menu::Cancel, | ||
| 405 | window: &mut Window, | ||
| 406 | cx: &mut Context<Self>, | ||
| 407 | ) { | ||
| 408 | self.edited_path.take(); | ||
| 409 | self.focus_handle.focus(window); | ||
| 410 | cx.notify(); | ||
| 411 | } | ||
| 412 | |||
| 413 | fn confirm_variable_edit( | ||
| 414 | &mut self, | ||
| 415 | _: &menu::Confirm, | ||
| 416 | _window: &mut Window, | ||
| 417 | cx: &mut Context<Self>, | ||
| 418 | ) { | ||
| 419 | let res = maybe!({ | ||
| 420 | let (var_path, editor) = self.edited_path.take()?; | ||
| 421 | let state = self.entry_states.get(&var_path)?; | ||
| 422 | let variables_reference = state.parent_reference; | ||
| 423 | let name = var_path.leaf_name?; | ||
| 424 | let value = editor.read(cx).text(cx); | ||
| 425 | |||
| 426 | self.session.update(cx, |session, cx| { | ||
| 427 | session.set_variable_value(variables_reference, name.into(), value, cx) | ||
| 428 | }); | ||
| 429 | Some(()) | ||
| 430 | }); | ||
| 431 | |||
| 432 | if res.is_none() { | ||
| 433 | log::error!("Couldn't confirm variable edit because variable doesn't have a leaf name or a parent reference id"); | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | fn collapse_selected_entry( | ||
| 438 | &mut self, | ||
| 439 | _: &CollapseSelectedEntry, | ||
| 440 | _window: &mut Window, | ||
| 441 | cx: &mut Context<Self>, | ||
| 442 | ) { | ||
| 443 | if let Some(ref selected_entry) = self.selection { | ||
| 444 | let Some(entry_state) = self.entry_states.get_mut(selected_entry) else { | ||
| 445 | debug_panic!("Trying to toggle variable in variable list that has an no state"); | ||
| 446 | return; | ||
| 447 | }; | ||
| 448 | |||
| 449 | entry_state.is_expanded = false; | ||
| 450 | cx.notify(); | ||
| 451 | } | ||
| 452 | } | ||
| 453 | |||
| 454 | fn expand_selected_entry( | ||
| 455 | &mut self, | ||
| 456 | _: &ExpandSelectedEntry, | ||
| 457 | _window: &mut Window, | ||
| 458 | cx: &mut Context<Self>, | ||
| 459 | ) { | ||
| 460 | if let Some(ref selected_entry) = self.selection { | ||
| 461 | let Some(entry_state) = self.entry_states.get_mut(selected_entry) else { | ||
| 462 | debug_panic!("Trying to toggle variable in variable list that has an no state"); | ||
| 463 | return; | ||
| 464 | }; | ||
| 465 | |||
| 466 | entry_state.is_expanded = true; | ||
| 467 | cx.notify(); | ||
| 468 | } | ||
| 469 | } | ||
| 470 | |||
| 471 | fn deploy_variable_context_menu( | ||
| 472 | &mut self, | ||
| 473 | variable: ListEntry, | ||
| 474 | position: Point<Pixels>, | ||
| 475 | window: &mut Window, | ||
| 476 | cx: &mut Context<Self>, | ||
| 477 | ) { | ||
| 478 | let Some(dap_var) = variable.as_variable() else { | ||
| 479 | debug_panic!("Trying to open variable context menu on a scope"); | ||
| 480 | return; | ||
| 481 | }; | ||
| 482 | |||
| 483 | let variable_value = dap_var.value.clone(); | ||
| 484 | let variable_name = dap_var.name.clone(); | ||
| 485 | let this = cx.entity().clone(); | ||
| 486 | |||
| 487 | let context_menu = ContextMenu::build(window, cx, |menu, _, _| { | ||
| 488 | menu.entry("Copy name", None, move |_, cx| { | ||
| 489 | cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone())) | ||
| 490 | }) | ||
| 491 | .entry("Copy value", None, { | ||
| 492 | let variable_value = variable_value.clone(); | ||
| 493 | move |_, cx| { | ||
| 494 | cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) | ||
| 495 | } | ||
| 496 | }) | ||
| 497 | .entry("Set value", None, move |window, cx| { | ||
| 498 | this.update(cx, |variable_list, cx| { | ||
| 499 | let editor = Self::create_variable_editor(&variable_value, window, cx); | ||
| 500 | variable_list.edited_path = Some((variable.path.clone(), editor)); | ||
| 501 | |||
| 502 | cx.notify(); | ||
| 503 | }); | ||
| 504 | }) | ||
| 505 | }); | ||
| 506 | |||
| 507 | cx.focus_view(&context_menu, window); | ||
| 508 | let subscription = cx.subscribe_in( | ||
| 509 | &context_menu, | ||
| 510 | window, | ||
| 511 | |this, _, _: &DismissEvent, window, cx| { | ||
| 512 | if this.open_context_menu.as_ref().is_some_and(|context_menu| { | ||
| 513 | context_menu.0.focus_handle(cx).contains_focused(window, cx) | ||
| 514 | }) { | ||
| 515 | cx.focus_self(window); | ||
| 516 | } | ||
| 517 | this.open_context_menu.take(); | ||
| 518 | cx.notify(); | ||
| 519 | }, | ||
| 520 | ); | ||
| 521 | |||
| 522 | self.open_context_menu = Some((context_menu, position, subscription)); | ||
| 523 | } | ||
| 524 | |||
| 525 | #[track_caller] | ||
| 526 | #[cfg(any(test, feature = "test-support"))] | ||
| 527 | pub fn assert_visual_entries(&self, expected: Vec<&str>) { | ||
| 528 | const INDENT: &'static str = " "; | ||
| 529 | |||
| 530 | let entries = &self.entries; | ||
| 531 | let mut visual_entries = Vec::with_capacity(entries.len()); | ||
| 532 | for entry in entries { | ||
| 533 | let state = self | ||
| 534 | .entry_states | ||
| 535 | .get(&entry.path) | ||
| 536 | .expect("If there's a variable entry there has to be a state that goes with it"); | ||
| 537 | |||
| 538 | visual_entries.push(format!( | ||
| 539 | "{}{} {}{}", | ||
| 540 | INDENT.repeat(state.depth - 1), | ||
| 541 | if state.is_expanded { "v" } else { ">" }, | ||
| 542 | entry.dap_kind.name(), | ||
| 543 | if self.selection.as_ref() == Some(&entry.path) { | ||
| 544 | " <=== selected" | ||
| 545 | } else { | ||
| 546 | "" | ||
| 547 | } | ||
| 548 | )); | ||
| 549 | } | ||
| 550 | |||
| 551 | pretty_assertions::assert_eq!(expected, visual_entries); | ||
| 552 | } | ||
| 553 | |||
| 554 | #[track_caller] | ||
| 555 | #[cfg(any(test, feature = "test-support"))] | ||
| 556 | pub fn scopes(&self) -> Vec<dap::Scope> { | ||
| 557 | self.entries | ||
| 558 | .iter() | ||
| 559 | .filter_map(|entry| match &entry.dap_kind { | ||
| 560 | EntryKind::Scope(scope) => Some(scope), | ||
| 561 | _ => None, | ||
| 562 | }) | ||
| 563 | .cloned() | ||
| 564 | .collect() | ||
| 565 | } | ||
| 566 | |||
| 567 | #[track_caller] | ||
| 568 | #[cfg(any(test, feature = "test-support"))] | ||
| 569 | pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> { | ||
| 570 | let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new(); | ||
| 571 | let mut idx = 0; | ||
| 572 | |||
| 573 | for entry in self.entries.iter() { | ||
| 574 | match &entry.dap_kind { | ||
| 575 | EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()), | ||
| 576 | EntryKind::Scope(scope) => { | ||
| 577 | if scopes.len() > 0 { | ||
| 578 | idx += 1; | ||
| 579 | } | ||
| 580 | |||
| 581 | scopes.push((scope.clone(), Vec::new())); | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } | ||
| 585 | |||
| 586 | scopes | ||
| 587 | } | ||
| 588 | |||
| 589 | #[track_caller] | ||
| 590 | #[cfg(any(test, feature = "test-support"))] | ||
| 591 | pub fn variables(&self) -> Vec<dap::Variable> { | ||
| 592 | self.entries | ||
| 593 | .iter() | ||
| 594 | .filter_map(|entry| match &entry.dap_kind { | ||
| 595 | EntryKind::Variable(variable) => Some(variable), | ||
| 596 | _ => None, | ||
| 597 | }) | ||
| 598 | .cloned() | ||
| 599 | .collect() | ||
| 600 | } | ||
| 601 | |||
| 602 | fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> { | ||
| 603 | let editor = cx.new(|cx| { | ||
| 604 | let mut editor = Editor::single_line(window, cx); | ||
| 605 | |||
| 606 | let refinement = TextStyleRefinement { | ||
| 607 | font_size: Some( | ||
| 608 | TextSize::XSmall | ||
| 609 | .rems(cx) | ||
| 610 | .to_pixels(window.rem_size()) | ||
| 611 | .into(), | ||
| 612 | ), | ||
| 613 | ..Default::default() | ||
| 614 | }; | ||
| 615 | editor.set_text_style_refinement(refinement); | ||
| 616 | editor.set_text(default, window, cx); | ||
| 617 | editor.select_all(&editor::actions::SelectAll, window, cx); | ||
| 618 | editor | ||
| 619 | }); | ||
| 620 | editor.focus_handle(cx).focus(window); | ||
| 621 | editor | ||
| 622 | } | ||
| 623 | |||
| 624 | fn render_scope( | ||
| 625 | &self, | ||
| 626 | entry: &ListEntry, | ||
| 627 | state: EntryState, | ||
| 628 | cx: &mut Context<Self>, | ||
| 629 | ) -> AnyElement { | ||
| 630 | let Some(scope) = entry.as_scope() else { | ||
| 631 | debug_panic!("Called render scope on non scope variable list entry variant"); | ||
| 632 | return div().into_any_element(); | ||
| 633 | }; | ||
| 634 | |||
| 635 | let var_ref = scope.variables_reference; | ||
| 636 | let is_selected = self | ||
| 637 | .selection | ||
| 638 | .as_ref() | ||
| 639 | .is_some_and(|selection| selection == &entry.path); | ||
| 640 | |||
| 641 | let colors = get_entry_color(cx); | ||
| 642 | let bg_hover_color = if !is_selected { | ||
| 643 | colors.hover | ||
| 644 | } else { | ||
| 645 | colors.default | ||
| 646 | }; | ||
| 647 | let border_color = if is_selected { | ||
| 648 | colors.marked_active | ||
| 649 | } else { | ||
| 650 | colors.default | ||
| 651 | }; | ||
| 652 | |||
| 653 | div() | ||
| 654 | .id(var_ref as usize) | ||
| 655 | .group("variable_list_entry") | ||
| 656 | .border_1() | ||
| 657 | .border_r_2() | ||
| 658 | .border_color(border_color) | ||
| 659 | .flex() | ||
| 660 | .w_full() | ||
| 661 | .h_full() | ||
| 662 | .hover(|style| style.bg(bg_hover_color)) | ||
| 663 | .on_click(cx.listener({ | ||
| 664 | move |_this, _, _window, cx| { | ||
| 665 | cx.notify(); | ||
| 666 | } | ||
| 667 | })) | ||
| 668 | .child( | ||
| 669 | ListItem::new(SharedString::from(format!("scope-{}", var_ref))) | ||
| 670 | .selectable(false) | ||
| 671 | .indent_level(state.depth + 1) | ||
| 672 | .indent_step_size(px(20.)) | ||
| 673 | .always_show_disclosure_icon(true) | ||
| 674 | .toggle(state.is_expanded) | ||
| 675 | .on_toggle({ | ||
| 676 | let var_path = entry.path.clone(); | ||
| 677 | cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx)) | ||
| 678 | }) | ||
| 679 | .child(div().text_ui(cx).w_full().child(scope.name.clone())), | ||
| 680 | ) | ||
| 681 | .into_any() | ||
| 682 | } | ||
| 683 | |||
| 684 | fn render_variable( | ||
| 685 | &self, | ||
| 686 | variable: &ListEntry, | ||
| 687 | state: EntryState, | ||
| 688 | window: &mut Window, | ||
| 689 | cx: &mut Context<Self>, | ||
| 690 | ) -> AnyElement { | ||
| 691 | let dap = match &variable.dap_kind { | ||
| 692 | EntryKind::Variable(dap) => dap, | ||
| 693 | EntryKind::Scope(_) => { | ||
| 694 | debug_panic!("Called render variable on variable list entry kind scope"); | ||
| 695 | return div().into_any_element(); | ||
| 696 | } | ||
| 697 | }; | ||
| 698 | |||
| 699 | let syntax_color_for = |name| cx.theme().syntax().get(name).color; | ||
| 700 | let variable_name_color = match &dap | ||
| 701 | .presentation_hint | ||
| 702 | .as_ref() | ||
| 703 | .and_then(|hint| hint.kind.as_ref()) | ||
| 704 | .unwrap_or(&VariablePresentationHintKind::Unknown) | ||
| 705 | { | ||
| 706 | VariablePresentationHintKind::Class | ||
| 707 | | VariablePresentationHintKind::BaseClass | ||
| 708 | | VariablePresentationHintKind::InnerClass | ||
| 709 | | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"), | ||
| 710 | VariablePresentationHintKind::Data => syntax_color_for("variable"), | ||
| 711 | VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"), | ||
| 712 | }; | ||
| 713 | let variable_color = syntax_color_for("variable.special"); | ||
| 714 | |||
| 715 | let var_ref = dap.variables_reference; | ||
| 716 | let colors = get_entry_color(cx); | ||
| 717 | let is_selected = self | ||
| 718 | .selection | ||
| 719 | .as_ref() | ||
| 720 | .is_some_and(|selected_path| *selected_path == variable.path); | ||
| 721 | |||
| 722 | let bg_hover_color = if !is_selected { | ||
| 723 | colors.hover | ||
| 724 | } else { | ||
| 725 | colors.default | ||
| 726 | }; | ||
| 727 | let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) { | ||
| 728 | colors.marked_active | ||
| 729 | } else { | ||
| 730 | colors.default | ||
| 731 | }; | ||
| 732 | let path = variable.path.clone(); | ||
| 733 | div() | ||
| 734 | .id(variable.item_id()) | ||
| 735 | .group("variable_list_entry") | ||
| 736 | .border_1() | ||
| 737 | .border_r_2() | ||
| 738 | .border_color(border_color) | ||
| 739 | .h_4() | ||
| 740 | .size_full() | ||
| 741 | .hover(|style| style.bg(bg_hover_color)) | ||
| 742 | .on_click(cx.listener({ | ||
| 743 | move |this, _, _window, cx| { | ||
| 744 | this.selection = Some(path.clone()); | ||
| 745 | cx.notify(); | ||
| 746 | } | ||
| 747 | })) | ||
| 748 | .child( | ||
| 749 | ListItem::new(SharedString::from(format!( | ||
| 750 | "variable-item-{}-{}", | ||
| 751 | dap.name, state.depth | ||
| 752 | ))) | ||
| 753 | .disabled(self.disabled) | ||
| 754 | .selectable(false) | ||
| 755 | .indent_level(state.depth + 1_usize) | ||
| 756 | .indent_step_size(px(20.)) | ||
| 757 | .always_show_disclosure_icon(true) | ||
| 758 | .when(var_ref > 0, |list_item| { | ||
| 759 | list_item.toggle(state.is_expanded).on_toggle(cx.listener({ | ||
| 760 | let var_path = variable.path.clone(); | ||
| 761 | move |this, _, _, cx| { | ||
| 762 | this.session.update(cx, |session, cx| { | ||
| 763 | session.variables(var_ref, cx); | ||
| 764 | }); | ||
| 765 | |||
| 766 | this.toggle_entry(&var_path, cx); | ||
| 767 | } | ||
| 768 | })) | ||
| 769 | }) | ||
| 770 | .on_secondary_mouse_down(cx.listener({ | ||
| 771 | let variable = variable.clone(); | ||
| 772 | move |this, event: &MouseDownEvent, window, cx| { | ||
| 773 | this.deploy_variable_context_menu( | ||
| 774 | variable.clone(), | ||
| 775 | event.position, | ||
| 776 | window, | ||
| 777 | cx, | ||
| 778 | ) | ||
| 779 | } | ||
| 780 | })) | ||
| 781 | .child( | ||
| 782 | h_flex() | ||
| 783 | .gap_1() | ||
| 784 | .text_ui_sm(cx) | ||
| 785 | .w_full() | ||
| 786 | .child( | ||
| 787 | Label::new(&dap.name).when_some(variable_name_color, |this, color| { | ||
| 788 | this.color(Color::from(color)) | ||
| 789 | }), | ||
| 790 | ) | ||
| 791 | .when(!dap.value.is_empty(), |this| { | ||
| 792 | this.child(div().w_full().id(variable.item_value_id()).map(|this| { | ||
| 793 | if let Some((_, editor)) = self | ||
| 794 | .edited_path | ||
| 795 | .as_ref() | ||
| 796 | .filter(|(path, _)| path == &variable.path) | ||
| 797 | { | ||
| 798 | this.child(div().size_full().px_2().child(editor.clone())) | ||
| 799 | } else { | ||
| 800 | this.text_color(cx.theme().colors().text_muted) | ||
| 801 | .when( | ||
| 802 | !self.disabled | ||
| 803 | && self | ||
| 804 | .session | ||
| 805 | .read(cx) | ||
| 806 | .capabilities() | ||
| 807 | .supports_set_variable | ||
| 808 | .unwrap_or_default(), | ||
| 809 | |this| { | ||
| 810 | let path = variable.path.clone(); | ||
| 811 | let variable_value = dap.value.clone(); | ||
| 812 | this.on_click(cx.listener( | ||
| 813 | move |this, click: &ClickEvent, window, cx| { | ||
| 814 | if click.down.click_count < 2 { | ||
| 815 | return; | ||
| 816 | } | ||
| 817 | let editor = Self::create_variable_editor( | ||
| 818 | &variable_value, | ||
| 819 | window, | ||
| 820 | cx, | ||
| 821 | ); | ||
| 822 | this.edited_path = | ||
| 823 | Some((path.clone(), editor)); | ||
| 824 | |||
| 825 | cx.notify(); | ||
| 826 | }, | ||
| 827 | )) | ||
| 828 | }, | ||
| 829 | ) | ||
| 830 | .child( | ||
| 831 | Label::new(format!("= {}", &dap.value)) | ||
| 832 | .single_line() | ||
| 833 | .truncate() | ||
| 834 | .size(LabelSize::Small) | ||
| 835 | .when_some(variable_color, |this, color| { | ||
| 836 | this.color(Color::from(color)) | ||
| 837 | }), | ||
| 838 | ) | ||
| 839 | } | ||
| 840 | })) | ||
| 841 | }), | ||
| 842 | ), | ||
| 843 | ) | ||
| 844 | .into_any() | ||
| 845 | } | ||
| 846 | |||
| 847 | fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> { | ||
| 848 | div() | ||
| 849 | .occlude() | ||
| 850 | .id("variable-list-vertical-scrollbar") | ||
| 851 | .on_mouse_move(cx.listener(|_, _, _, cx| { | ||
| 852 | cx.notify(); | ||
| 853 | cx.stop_propagation() | ||
| 854 | })) | ||
| 855 | .on_hover(|_, _, cx| { | ||
| 856 | cx.stop_propagation(); | ||
| 857 | }) | ||
| 858 | .on_any_mouse_down(|_, _, cx| { | ||
| 859 | cx.stop_propagation(); | ||
| 860 | }) | ||
| 861 | .on_mouse_up( | ||
| 862 | MouseButton::Left, | ||
| 863 | cx.listener(|_, _, _, cx| { | ||
| 864 | cx.stop_propagation(); | ||
| 865 | }), | ||
| 866 | ) | ||
| 867 | .on_scroll_wheel(cx.listener(|_, _, _, cx| { | ||
| 868 | cx.notify(); | ||
| 869 | })) | ||
| 870 | .h_full() | ||
| 871 | .absolute() | ||
| 872 | .right_1() | ||
| 873 | .top_1() | ||
| 874 | .bottom_0() | ||
| 875 | .w(px(12.)) | ||
| 876 | .cursor_default() | ||
| 877 | .children(Scrollbar::vertical(self.scrollbar_state.clone())) | ||
| 878 | } | ||
| 879 | } | ||
| 880 | |||
| 881 | impl Focusable for VariableList { | ||
| 882 | fn focus_handle(&self, _: &App) -> gpui::FocusHandle { | ||
| 883 | self.focus_handle.clone() | ||
| 884 | } | ||
| 885 | } | ||
| 886 | |||
| 887 | impl Render for VariableList { | ||
| 888 | fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 889 | self.build_entries(cx); | ||
| 890 | |||
| 891 | v_flex() | ||
| 892 | .key_context("VariableList") | ||
| 893 | .id("variable-list") | ||
| 894 | .group("variable-list") | ||
| 895 | .overflow_y_scroll() | ||
| 896 | .size_full() | ||
| 897 | .track_focus(&self.focus_handle(cx)) | ||
| 898 | .on_action(cx.listener(Self::select_first)) | ||
| 899 | .on_action(cx.listener(Self::select_last)) | ||
| 900 | .on_action(cx.listener(Self::select_prev)) | ||
| 901 | .on_action(cx.listener(Self::select_next)) | ||
| 902 | .on_action(cx.listener(Self::expand_selected_entry)) | ||
| 903 | .on_action(cx.listener(Self::collapse_selected_entry)) | ||
| 904 | .on_action(cx.listener(Self::cancel_variable_edit)) | ||
| 905 | .on_action(cx.listener(Self::confirm_variable_edit)) | ||
| 906 | // | ||
| 907 | .child( | ||
| 908 | uniform_list( | ||
| 909 | cx.entity().clone(), | ||
| 910 | "variable-list", | ||
| 911 | self.entries.len(), | ||
| 912 | move |this, range, window, cx| this.render_entries(range, window, cx), | ||
| 913 | ) | ||
| 914 | .track_scroll(self.list_handle.clone()) | ||
| 915 | .gap_1_5() | ||
| 916 | .size_full() | ||
| 917 | .flex_grow(), | ||
| 918 | ) | ||
| 919 | .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { | ||
| 920 | deferred( | ||
| 921 | anchored() | ||
| 922 | .position(*position) | ||
| 923 | .anchor(gpui::Corner::TopLeft) | ||
| 924 | .child(menu.clone()), | ||
| 925 | ) | ||
| 926 | .with_priority(1) | ||
| 927 | })) | ||
| 928 | .child(self.render_vertical_scrollbar(cx)) | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | struct EntryColors { | ||
| 933 | default: Hsla, | ||
| 934 | hover: Hsla, | ||
| 935 | marked_active: Hsla, | ||
| 936 | } | ||
| 937 | |||
| 938 | fn get_entry_color(cx: &Context<VariableList>) -> EntryColors { | ||
| 939 | let colors = cx.theme().colors(); | ||
| 940 | |||
| 941 | EntryColors { | ||
| 942 | default: colors.panel_background, | ||
| 943 | hover: colors.ghost_element_hover, | ||
| 944 | marked_active: colors.ghost_element_selected, | ||
| 945 | } | ||
| 946 | } | ||
| @@ -0,0 +1,80 @@ | |||
| 1 | use std::time::Duration; | ||
| 2 | |||
| 3 | use anyhow::Result; | ||
| 4 | |||
| 5 | use dap::client::SessionId; | ||
| 6 | use gpui::{ | ||
| 7 | percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task, | ||
| 8 | Transformation, | ||
| 9 | }; | ||
| 10 | use project::debugger::session::Session; | ||
| 11 | use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled}; | ||
| 12 | |||
| 13 | pub(crate) struct StartingState { | ||
| 14 | focus_handle: FocusHandle, | ||
| 15 | pub(super) session_id: SessionId, | ||
| 16 | _notify_parent: Task<()>, | ||
| 17 | } | ||
| 18 | |||
| 19 | pub(crate) enum StartingEvent { | ||
| 20 | Failed, | ||
| 21 | Finished(Entity<Session>), | ||
| 22 | } | ||
| 23 | |||
| 24 | impl EventEmitter<StartingEvent> for StartingState {} | ||
| 25 | |||
| 26 | impl StartingState { | ||
| 27 | pub(crate) fn new( | ||
| 28 | session_id: SessionId, | ||
| 29 | task: Task<Result<Entity<Session>>>, | ||
| 30 | cx: &mut Context<Self>, | ||
| 31 | ) -> Self { | ||
| 32 | let _notify_parent = cx.spawn(move |this, mut cx| async move { | ||
| 33 | let entity = task.await; | ||
| 34 | |||
| 35 | this.update(&mut cx, |_, cx| { | ||
| 36 | if let Ok(entity) = entity { | ||
| 37 | cx.emit(StartingEvent::Finished(entity)) | ||
| 38 | } else { | ||
| 39 | cx.emit(StartingEvent::Failed) | ||
| 40 | } | ||
| 41 | }) | ||
| 42 | .ok(); | ||
| 43 | }); | ||
| 44 | Self { | ||
| 45 | session_id, | ||
| 46 | focus_handle: cx.focus_handle(), | ||
| 47 | _notify_parent, | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | impl Focusable for StartingState { | ||
| 53 | fn focus_handle(&self, _: &ui::App) -> FocusHandle { | ||
| 54 | self.focus_handle.clone() | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | impl Render for StartingState { | ||
| 59 | fn render( | ||
| 60 | &mut self, | ||
| 61 | _window: &mut ui::Window, | ||
| 62 | _cx: &mut ui::Context<'_, Self>, | ||
| 63 | ) -> impl ui::IntoElement { | ||
| 64 | v_flex() | ||
| 65 | .size_full() | ||
| 66 | .gap_1() | ||
| 67 | .items_center() | ||
| 68 | .child("Starting a debug adapter") | ||
| 69 | .child( | ||
| 70 | Icon::new(IconName::ArrowCircle) | ||
| 71 | .color(Color::Info) | ||
| 72 | .with_animation( | ||
| 73 | "arrow-circle", | ||
| 74 | Animation::new(Duration::from_secs(2)).repeat(), | ||
| 75 | |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), | ||
| 76 | ) | ||
| 77 | .into_any_element(), | ||
| 78 | ) | ||
| 79 | } | ||
| 80 | } | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | use gpui::{Entity, TestAppContext, WindowHandle}; | ||
| 2 | use project::Project; | ||
| 3 | use settings::SettingsStore; | ||
| 4 | use terminal_view::terminal_panel::TerminalPanel; | ||
| 5 | use workspace::Workspace; | ||
| 6 | |||
| 7 | use crate::{debugger_panel::DebugPanel, session::DebugSession}; | ||
| 8 | |||
| 9 | mod attach_modal; | ||
| 10 | mod console; | ||
| 11 | mod debugger_panel; | ||
| 12 | mod module_list; | ||
| 13 | mod stack_frame_list; | ||
| 14 | mod variable_list; | ||
| 15 | |||
| 16 | pub fn init_test(cx: &mut gpui::TestAppContext) { | ||
| 17 | if std::env::var("RUST_LOG").is_ok() { | ||
| 18 | env_logger::try_init().ok(); | ||
| 19 | } | ||
| 20 | |||
| 21 | cx.update(|cx| { | ||
| 22 | let settings = SettingsStore::test(cx); | ||
| 23 | cx.set_global(settings); | ||
| 24 | terminal_view::init(cx); | ||
| 25 | theme::init(theme::LoadThemes::JustBase, cx); | ||
| 26 | command_palette_hooks::init(cx); | ||
| 27 | language::init(cx); | ||
| 28 | workspace::init_settings(cx); | ||
| 29 | Project::init_settings(cx); | ||
| 30 | editor::init(cx); | ||
| 31 | crate::init(cx); | ||
| 32 | }); | ||
| 33 | } | ||
| 34 | |||
| 35 | pub async fn init_test_workspace( | ||
| 36 | project: &Entity<Project>, | ||
| 37 | cx: &mut TestAppContext, | ||
| 38 | ) -> WindowHandle<Workspace> { | ||
| 39 | let workspace_handle = | ||
| 40 | cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); | ||
| 41 | |||
| 42 | let debugger_panel = workspace_handle | ||
| 43 | .update(cx, |_, window, cx| cx.spawn_in(window, DebugPanel::load)) | ||
| 44 | .unwrap() | ||
| 45 | .await | ||
| 46 | .expect("Failed to load debug panel"); | ||
| 47 | |||
| 48 | let terminal_panel = workspace_handle | ||
| 49 | .update(cx, |_, window, cx| cx.spawn_in(window, TerminalPanel::load)) | ||
| 50 | .unwrap() | ||
| 51 | .await | ||
| 52 | .expect("Failed to load terminal panel"); | ||
| 53 | |||
| 54 | workspace_handle | ||
| 55 | .update(cx, |workspace, window, cx| { | ||
| 56 | workspace.add_panel(debugger_panel, window, cx); | ||
| 57 | workspace.add_panel(terminal_panel, window, cx); | ||
| 58 | }) | ||
| 59 | .unwrap(); | ||
| 60 | workspace_handle | ||
| 61 | } | ||
| 62 | |||
| 63 | pub fn active_debug_session_panel( | ||
| 64 | workspace: WindowHandle<Workspace>, | ||
| 65 | cx: &mut TestAppContext, | ||
| 66 | ) -> Entity<DebugSession> { | ||
| 67 | workspace | ||
| 68 | .update(cx, |workspace, _window, cx| { | ||
| 69 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 70 | debug_panel | ||
| 71 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 72 | .unwrap() | ||
| 73 | }) | ||
| 74 | .unwrap() | ||
| 75 | } | ||
| @@ -0,0 +1,136 @@ | |||
| 1 | use crate::*; | ||
| 2 | use attach_modal::AttachModal; | ||
| 3 | use dap::client::SessionId; | ||
| 4 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 5 | use menu::Confirm; | ||
| 6 | use project::{FakeFs, Project}; | ||
| 7 | use serde_json::json; | ||
| 8 | use task::AttachConfig; | ||
| 9 | use tests::{init_test, init_test_workspace}; | ||
| 10 | |||
| 11 | #[gpui::test] | ||
| 12 | async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 13 | init_test(cx); | ||
| 14 | |||
| 15 | let fs = FakeFs::new(executor.clone()); | ||
| 16 | |||
| 17 | fs.insert_tree( | ||
| 18 | "/project", | ||
| 19 | json!({ | ||
| 20 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 21 | }), | ||
| 22 | ) | ||
| 23 | .await; | ||
| 24 | |||
| 25 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 26 | let workspace = init_test_workspace(&project, cx).await; | ||
| 27 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 28 | |||
| 29 | let task = project.update(cx, |project, cx| { | ||
| 30 | project.start_debug_session( | ||
| 31 | dap::test_config( | ||
| 32 | dap::DebugRequestType::Attach(AttachConfig { | ||
| 33 | process_id: Some(10), | ||
| 34 | }), | ||
| 35 | None, | ||
| 36 | None, | ||
| 37 | ), | ||
| 38 | cx, | ||
| 39 | ) | ||
| 40 | }); | ||
| 41 | |||
| 42 | let session = task.await.unwrap(); | ||
| 43 | |||
| 44 | cx.run_until_parked(); | ||
| 45 | |||
| 46 | // assert we didn't show the attach modal | ||
| 47 | workspace | ||
| 48 | .update(cx, |workspace, _window, cx| { | ||
| 49 | assert!(workspace.active_modal::<AttachModal>(cx).is_none()); | ||
| 50 | }) | ||
| 51 | .unwrap(); | ||
| 52 | |||
| 53 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 54 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 55 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 56 | }) | ||
| 57 | }); | ||
| 58 | |||
| 59 | shutdown_session.await.unwrap(); | ||
| 60 | } | ||
| 61 | |||
| 62 | #[gpui::test] | ||
| 63 | async fn test_show_attach_modal_and_select_process( | ||
| 64 | executor: BackgroundExecutor, | ||
| 65 | cx: &mut TestAppContext, | ||
| 66 | ) { | ||
| 67 | init_test(cx); | ||
| 68 | |||
| 69 | let fs = FakeFs::new(executor.clone()); | ||
| 70 | |||
| 71 | fs.insert_tree( | ||
| 72 | "/project", | ||
| 73 | json!({ | ||
| 74 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 75 | }), | ||
| 76 | ) | ||
| 77 | .await; | ||
| 78 | |||
| 79 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 80 | let workspace = init_test_workspace(&project, cx).await; | ||
| 81 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 82 | |||
| 83 | let attach_modal = workspace | ||
| 84 | .update(cx, |workspace, window, cx| { | ||
| 85 | workspace.toggle_modal(window, cx, |window, cx| { | ||
| 86 | AttachModal::new( | ||
| 87 | project.clone(), | ||
| 88 | dap::test_config( | ||
| 89 | dap::DebugRequestType::Attach(AttachConfig { process_id: None }), | ||
| 90 | None, | ||
| 91 | None, | ||
| 92 | ), | ||
| 93 | window, | ||
| 94 | cx, | ||
| 95 | ) | ||
| 96 | }); | ||
| 97 | |||
| 98 | workspace.active_modal::<AttachModal>(cx).unwrap() | ||
| 99 | }) | ||
| 100 | .unwrap(); | ||
| 101 | |||
| 102 | cx.run_until_parked(); | ||
| 103 | |||
| 104 | // assert we got the expected processes | ||
| 105 | workspace | ||
| 106 | .update(cx, |_, _, cx| { | ||
| 107 | let names = | ||
| 108 | attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx)); | ||
| 109 | |||
| 110 | // we filtered out all processes that are not the current process(zed itself) | ||
| 111 | assert_eq!(1, names.len()); | ||
| 112 | }) | ||
| 113 | .unwrap(); | ||
| 114 | |||
| 115 | // select the only existing process | ||
| 116 | cx.dispatch_action(Confirm); | ||
| 117 | |||
| 118 | cx.run_until_parked(); | ||
| 119 | |||
| 120 | // assert attach modal was dismissed | ||
| 121 | workspace | ||
| 122 | .update(cx, |workspace, _window, cx| { | ||
| 123 | assert!(workspace.active_modal::<AttachModal>(cx).is_none()); | ||
| 124 | }) | ||
| 125 | .unwrap(); | ||
| 126 | |||
| 127 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 128 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 129 | let session = dap_store.session_by_id(SessionId(0)).unwrap(); | ||
| 130 | |||
| 131 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 132 | }) | ||
| 133 | }); | ||
| 134 | |||
| 135 | shutdown_session.await.unwrap(); | ||
| 136 | } | ||
| @@ -0,0 +1,916 @@ | |||
| 1 | use crate::{tests::active_debug_session_panel, *}; | ||
| 2 | use dap::requests::StackTrace; | ||
| 3 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 4 | use project::{FakeFs, Project}; | ||
| 5 | use serde_json::json; | ||
| 6 | use tests::{init_test, init_test_workspace}; | ||
| 7 | |||
| 8 | #[gpui::test] | ||
| 9 | async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 10 | init_test(cx); | ||
| 11 | |||
| 12 | let fs = FakeFs::new(executor.clone()); | ||
| 13 | |||
| 14 | fs.insert_tree( | ||
| 15 | "/project", | ||
| 16 | json!({ | ||
| 17 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 18 | }), | ||
| 19 | ) | ||
| 20 | .await; | ||
| 21 | |||
| 22 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 23 | let workspace = init_test_workspace(&project, cx).await; | ||
| 24 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 25 | workspace | ||
| 26 | .update(cx, |workspace, window, cx| { | ||
| 27 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 28 | }) | ||
| 29 | .unwrap(); | ||
| 30 | |||
| 31 | let task = project.update(cx, |project, cx| { | ||
| 32 | project.start_debug_session( | ||
| 33 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 34 | cx, | ||
| 35 | ) | ||
| 36 | }); | ||
| 37 | |||
| 38 | let session = task.await.unwrap(); | ||
| 39 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 40 | |||
| 41 | client | ||
| 42 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 43 | Ok(dap::StackTraceResponse { | ||
| 44 | stack_frames: Vec::default(), | ||
| 45 | total_frames: None, | ||
| 46 | }) | ||
| 47 | }) | ||
| 48 | .await; | ||
| 49 | |||
| 50 | client | ||
| 51 | .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 52 | category: None, | ||
| 53 | output: "First console output line before thread stopped!".to_string(), | ||
| 54 | data: None, | ||
| 55 | variables_reference: None, | ||
| 56 | source: None, | ||
| 57 | line: None, | ||
| 58 | column: None, | ||
| 59 | group: None, | ||
| 60 | location_reference: None, | ||
| 61 | })) | ||
| 62 | .await; | ||
| 63 | |||
| 64 | client | ||
| 65 | .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 66 | category: Some(dap::OutputEventCategory::Stdout), | ||
| 67 | output: "First output line before thread stopped!".to_string(), | ||
| 68 | data: None, | ||
| 69 | variables_reference: None, | ||
| 70 | source: None, | ||
| 71 | line: None, | ||
| 72 | column: None, | ||
| 73 | group: None, | ||
| 74 | location_reference: None, | ||
| 75 | })) | ||
| 76 | .await; | ||
| 77 | |||
| 78 | client | ||
| 79 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 80 | reason: dap::StoppedEventReason::Pause, | ||
| 81 | description: None, | ||
| 82 | thread_id: Some(1), | ||
| 83 | preserve_focus_hint: None, | ||
| 84 | text: None, | ||
| 85 | all_threads_stopped: None, | ||
| 86 | hit_breakpoint_ids: None, | ||
| 87 | })) | ||
| 88 | .await; | ||
| 89 | |||
| 90 | cx.run_until_parked(); | ||
| 91 | |||
| 92 | let running_state = | ||
| 93 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 94 | cx.focus_self(window); | ||
| 95 | item.mode() | ||
| 96 | .as_running() | ||
| 97 | .expect("Session should be running by this point") | ||
| 98 | .clone() | ||
| 99 | }); | ||
| 100 | |||
| 101 | running_state.update(cx, |state, cx| { | ||
| 102 | state.set_thread_item(session::ThreadItem::Console, cx); | ||
| 103 | cx.refresh_windows(); | ||
| 104 | }); | ||
| 105 | cx.run_until_parked(); | ||
| 106 | |||
| 107 | // assert we have output from before the thread stopped | ||
| 108 | workspace | ||
| 109 | .update(cx, |workspace, _window, cx| { | ||
| 110 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 111 | let active_debug_session_panel = debug_panel | ||
| 112 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 113 | .unwrap(); | ||
| 114 | |||
| 115 | assert_eq!( | ||
| 116 | "First console output line before thread stopped!\nFirst output line before thread stopped!\n", | ||
| 117 | active_debug_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str() | ||
| 118 | ); | ||
| 119 | }) | ||
| 120 | .unwrap(); | ||
| 121 | |||
| 122 | client | ||
| 123 | .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 124 | category: Some(dap::OutputEventCategory::Stdout), | ||
| 125 | output: "Second output line after thread stopped!".to_string(), | ||
| 126 | data: None, | ||
| 127 | variables_reference: None, | ||
| 128 | source: None, | ||
| 129 | line: None, | ||
| 130 | column: None, | ||
| 131 | group: None, | ||
| 132 | location_reference: None, | ||
| 133 | })) | ||
| 134 | .await; | ||
| 135 | |||
| 136 | client | ||
| 137 | .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 138 | category: Some(dap::OutputEventCategory::Console), | ||
| 139 | output: "Second console output line after thread stopped!".to_string(), | ||
| 140 | data: None, | ||
| 141 | variables_reference: None, | ||
| 142 | source: None, | ||
| 143 | line: None, | ||
| 144 | column: None, | ||
| 145 | group: None, | ||
| 146 | location_reference: None, | ||
| 147 | })) | ||
| 148 | .await; | ||
| 149 | |||
| 150 | cx.run_until_parked(); | ||
| 151 | running_state.update(cx, |state, cx| { | ||
| 152 | state.set_thread_item(session::ThreadItem::Console, cx); | ||
| 153 | cx.refresh_windows(); | ||
| 154 | }); | ||
| 155 | cx.run_until_parked(); | ||
| 156 | |||
| 157 | // assert we have output from before and after the thread stopped | ||
| 158 | workspace | ||
| 159 | .update(cx, |workspace, _window, cx| { | ||
| 160 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 161 | let active_session_panel = debug_panel | ||
| 162 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 163 | .unwrap(); | ||
| 164 | |||
| 165 | assert_eq!( | ||
| 166 | "First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n", | ||
| 167 | active_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str() | ||
| 168 | ); | ||
| 169 | }) | ||
| 170 | .unwrap(); | ||
| 171 | |||
| 172 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 173 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 174 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 175 | }) | ||
| 176 | }); | ||
| 177 | |||
| 178 | shutdown_session.await.unwrap(); | ||
| 179 | } | ||
| 180 | |||
| 181 | // #[gpui::test] | ||
| 182 | // async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 183 | // init_test(cx); | ||
| 184 | |||
| 185 | // let fs = FakeFs::new(executor.clone()); | ||
| 186 | |||
| 187 | // fs.insert_tree( | ||
| 188 | // "/project", | ||
| 189 | // json!({ | ||
| 190 | // "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 191 | // }), | ||
| 192 | // ) | ||
| 193 | // .await; | ||
| 194 | |||
| 195 | // let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 196 | // let workspace = init_test_workspace(&project, cx).await; | ||
| 197 | // let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 198 | |||
| 199 | // let task = project.update(cx, |project, cx| { | ||
| 200 | // project.start_debug_session( | ||
| 201 | // dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 202 | // cx, | ||
| 203 | // ) | ||
| 204 | // }); | ||
| 205 | |||
| 206 | // let session = task.await.unwrap(); | ||
| 207 | // let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 208 | |||
| 209 | // client | ||
| 210 | // .on_request::<StackTrace, _>(move |_, _| { | ||
| 211 | // Ok(dap::StackTraceResponse { | ||
| 212 | // stack_frames: Vec::default(), | ||
| 213 | // total_frames: None, | ||
| 214 | // }) | ||
| 215 | // }) | ||
| 216 | // .await; | ||
| 217 | |||
| 218 | // client | ||
| 219 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 220 | // reason: dap::StoppedEventReason::Pause, | ||
| 221 | // description: None, | ||
| 222 | // thread_id: Some(1), | ||
| 223 | // preserve_focus_hint: None, | ||
| 224 | // text: None, | ||
| 225 | // all_threads_stopped: None, | ||
| 226 | // hit_breakpoint_ids: None, | ||
| 227 | // })) | ||
| 228 | // .await; | ||
| 229 | |||
| 230 | // client | ||
| 231 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 232 | // category: None, | ||
| 233 | // output: "First line".to_string(), | ||
| 234 | // data: None, | ||
| 235 | // variables_reference: None, | ||
| 236 | // source: None, | ||
| 237 | // line: None, | ||
| 238 | // column: None, | ||
| 239 | // group: None, | ||
| 240 | // location_reference: None, | ||
| 241 | // })) | ||
| 242 | // .await; | ||
| 243 | |||
| 244 | // client | ||
| 245 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 246 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 247 | // output: "First group".to_string(), | ||
| 248 | // data: None, | ||
| 249 | // variables_reference: None, | ||
| 250 | // source: None, | ||
| 251 | // line: None, | ||
| 252 | // column: None, | ||
| 253 | // group: Some(dap::OutputEventGroup::Start), | ||
| 254 | // location_reference: None, | ||
| 255 | // })) | ||
| 256 | // .await; | ||
| 257 | |||
| 258 | // client | ||
| 259 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 260 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 261 | // output: "First item in group 1".to_string(), | ||
| 262 | // data: None, | ||
| 263 | // variables_reference: None, | ||
| 264 | // source: None, | ||
| 265 | // line: None, | ||
| 266 | // column: None, | ||
| 267 | // group: None, | ||
| 268 | // location_reference: None, | ||
| 269 | // })) | ||
| 270 | // .await; | ||
| 271 | |||
| 272 | // client | ||
| 273 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 274 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 275 | // output: "Second item in group 1".to_string(), | ||
| 276 | // data: None, | ||
| 277 | // variables_reference: None, | ||
| 278 | // source: None, | ||
| 279 | // line: None, | ||
| 280 | // column: None, | ||
| 281 | // group: None, | ||
| 282 | // location_reference: None, | ||
| 283 | // })) | ||
| 284 | // .await; | ||
| 285 | |||
| 286 | // client | ||
| 287 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 288 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 289 | // output: "Second group".to_string(), | ||
| 290 | // data: None, | ||
| 291 | // variables_reference: None, | ||
| 292 | // source: None, | ||
| 293 | // line: None, | ||
| 294 | // column: None, | ||
| 295 | // group: Some(dap::OutputEventGroup::Start), | ||
| 296 | // location_reference: None, | ||
| 297 | // })) | ||
| 298 | // .await; | ||
| 299 | |||
| 300 | // client | ||
| 301 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 302 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 303 | // output: "First item in group 2".to_string(), | ||
| 304 | // data: None, | ||
| 305 | // variables_reference: None, | ||
| 306 | // source: None, | ||
| 307 | // line: None, | ||
| 308 | // column: None, | ||
| 309 | // group: None, | ||
| 310 | // location_reference: None, | ||
| 311 | // })) | ||
| 312 | // .await; | ||
| 313 | |||
| 314 | // client | ||
| 315 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 316 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 317 | // output: "Second item in group 2".to_string(), | ||
| 318 | // data: None, | ||
| 319 | // variables_reference: None, | ||
| 320 | // source: None, | ||
| 321 | // line: None, | ||
| 322 | // column: None, | ||
| 323 | // group: None, | ||
| 324 | // location_reference: None, | ||
| 325 | // })) | ||
| 326 | // .await; | ||
| 327 | |||
| 328 | // client | ||
| 329 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 330 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 331 | // output: "End group 2".to_string(), | ||
| 332 | // data: None, | ||
| 333 | // variables_reference: None, | ||
| 334 | // source: None, | ||
| 335 | // line: None, | ||
| 336 | // column: None, | ||
| 337 | // group: Some(dap::OutputEventGroup::End), | ||
| 338 | // location_reference: None, | ||
| 339 | // })) | ||
| 340 | // .await; | ||
| 341 | |||
| 342 | // client | ||
| 343 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 344 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 345 | // output: "Third group".to_string(), | ||
| 346 | // data: None, | ||
| 347 | // variables_reference: None, | ||
| 348 | // source: None, | ||
| 349 | // line: None, | ||
| 350 | // column: None, | ||
| 351 | // group: Some(dap::OutputEventGroup::StartCollapsed), | ||
| 352 | // location_reference: None, | ||
| 353 | // })) | ||
| 354 | // .await; | ||
| 355 | |||
| 356 | // client | ||
| 357 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 358 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 359 | // output: "First item in group 3".to_string(), | ||
| 360 | // data: None, | ||
| 361 | // variables_reference: None, | ||
| 362 | // source: None, | ||
| 363 | // line: None, | ||
| 364 | // column: None, | ||
| 365 | // group: None, | ||
| 366 | // location_reference: None, | ||
| 367 | // })) | ||
| 368 | // .await; | ||
| 369 | |||
| 370 | // client | ||
| 371 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 372 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 373 | // output: "Second item in group 3".to_string(), | ||
| 374 | // data: None, | ||
| 375 | // variables_reference: None, | ||
| 376 | // source: None, | ||
| 377 | // line: None, | ||
| 378 | // column: None, | ||
| 379 | // group: None, | ||
| 380 | // location_reference: None, | ||
| 381 | // })) | ||
| 382 | // .await; | ||
| 383 | |||
| 384 | // client | ||
| 385 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 386 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 387 | // output: "End group 3".to_string(), | ||
| 388 | // data: None, | ||
| 389 | // variables_reference: None, | ||
| 390 | // source: None, | ||
| 391 | // line: None, | ||
| 392 | // column: None, | ||
| 393 | // group: Some(dap::OutputEventGroup::End), | ||
| 394 | // location_reference: None, | ||
| 395 | // })) | ||
| 396 | // .await; | ||
| 397 | |||
| 398 | // client | ||
| 399 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 400 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 401 | // output: "Third item in group 1".to_string(), | ||
| 402 | // data: None, | ||
| 403 | // variables_reference: None, | ||
| 404 | // source: None, | ||
| 405 | // line: None, | ||
| 406 | // column: None, | ||
| 407 | // group: None, | ||
| 408 | // location_reference: None, | ||
| 409 | // })) | ||
| 410 | // .await; | ||
| 411 | |||
| 412 | // client | ||
| 413 | // .fake_event(dap::messages::Events::Output(dap::OutputEvent { | ||
| 414 | // category: Some(dap::OutputEventCategory::Stdout), | ||
| 415 | // output: "Second item".to_string(), | ||
| 416 | // data: None, | ||
| 417 | // variables_reference: None, | ||
| 418 | // source: None, | ||
| 419 | // line: None, | ||
| 420 | // column: None, | ||
| 421 | // group: Some(dap::OutputEventGroup::End), | ||
| 422 | // location_reference: None, | ||
| 423 | // })) | ||
| 424 | // .await; | ||
| 425 | |||
| 426 | // cx.run_until_parked(); | ||
| 427 | |||
| 428 | // active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| { | ||
| 429 | // debug_panel_item | ||
| 430 | // .mode() | ||
| 431 | // .as_running() | ||
| 432 | // .unwrap() | ||
| 433 | // .update(cx, |running_state, cx| { | ||
| 434 | // running_state.console().update(cx, |console, cx| { | ||
| 435 | // console.editor().update(cx, |editor, cx| { | ||
| 436 | // pretty_assertions::assert_eq!( | ||
| 437 | // " | ||
| 438 | // First line | ||
| 439 | // First group | ||
| 440 | // First item in group 1 | ||
| 441 | // Second item in group 1 | ||
| 442 | // Second group | ||
| 443 | // First item in group 2 | ||
| 444 | // Second item in group 2 | ||
| 445 | // End group 2 | ||
| 446 | // ⋯ End group 3 | ||
| 447 | // Third item in group 1 | ||
| 448 | // Second item | ||
| 449 | // " | ||
| 450 | // .unindent(), | ||
| 451 | // editor.display_text(cx) | ||
| 452 | // ); | ||
| 453 | // }) | ||
| 454 | // }); | ||
| 455 | // }); | ||
| 456 | // }); | ||
| 457 | |||
| 458 | // let shutdown_session = project.update(cx, |project, cx| { | ||
| 459 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 460 | // dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 461 | // }) | ||
| 462 | // }); | ||
| 463 | |||
| 464 | // shutdown_session.await.unwrap(); | ||
| 465 | // } | ||
| 466 | |||
| 467 | // todo(debugger): enable this again | ||
| 468 | // #[gpui::test] | ||
| 469 | // async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 470 | // init_test(cx); | ||
| 471 | |||
| 472 | // const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}"; | ||
| 473 | |||
| 474 | // let called_evaluate = Arc::new(AtomicBool::new(false)); | ||
| 475 | |||
| 476 | // let fs = FakeFs::new(executor.clone()); | ||
| 477 | |||
| 478 | // let test_file_content = r#" | ||
| 479 | // const variable1 = { | ||
| 480 | // nested1: "Nested 1", | ||
| 481 | // nested2: "Nested 2", | ||
| 482 | // }; | ||
| 483 | // const variable2 = "Value 2"; | ||
| 484 | // const variable3 = "Value 3"; | ||
| 485 | // "# | ||
| 486 | // .unindent(); | ||
| 487 | |||
| 488 | // fs.insert_tree( | ||
| 489 | // "/project", | ||
| 490 | // json!({ | ||
| 491 | // "src": { | ||
| 492 | // "test.js": test_file_content, | ||
| 493 | // } | ||
| 494 | // }), | ||
| 495 | // ) | ||
| 496 | // .await; | ||
| 497 | |||
| 498 | // let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 499 | // let workspace = init_test_workspace(&project, cx).await; | ||
| 500 | // let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 501 | |||
| 502 | // let task = project.update(cx, |project, cx| { | ||
| 503 | // project.start_debug_session(dap::test_config(None), cx) | ||
| 504 | // }); | ||
| 505 | |||
| 506 | // let session = task.await.unwrap(); | ||
| 507 | // let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 508 | |||
| 509 | // client | ||
| 510 | // .on_request::<Threads, _>(move |_, _| { | ||
| 511 | // Ok(dap::ThreadsResponse { | ||
| 512 | // threads: vec![dap::Thread { | ||
| 513 | // id: 1, | ||
| 514 | // name: "Thread 1".into(), | ||
| 515 | // }], | ||
| 516 | // }) | ||
| 517 | // }) | ||
| 518 | // .await; | ||
| 519 | |||
| 520 | // let stack_frames = vec![StackFrame { | ||
| 521 | // id: 1, | ||
| 522 | // name: "Stack Frame 1".into(), | ||
| 523 | // source: Some(dap::Source { | ||
| 524 | // name: Some("test.js".into()), | ||
| 525 | // path: Some("/project/src/test.js".into()), | ||
| 526 | // source_reference: None, | ||
| 527 | // presentation_hint: None, | ||
| 528 | // origin: None, | ||
| 529 | // sources: None, | ||
| 530 | // adapter_data: None, | ||
| 531 | // checksums: None, | ||
| 532 | // }), | ||
| 533 | // line: 3, | ||
| 534 | // column: 1, | ||
| 535 | // end_line: None, | ||
| 536 | // end_column: None, | ||
| 537 | // can_restart: None, | ||
| 538 | // instruction_pointer_reference: None, | ||
| 539 | // module_id: None, | ||
| 540 | // presentation_hint: None, | ||
| 541 | // }]; | ||
| 542 | |||
| 543 | // client | ||
| 544 | // .on_request::<StackTrace, _>({ | ||
| 545 | // let stack_frames = Arc::new(stack_frames.clone()); | ||
| 546 | // move |_, args| { | ||
| 547 | // assert_eq!(1, args.thread_id); | ||
| 548 | |||
| 549 | // Ok(dap::StackTraceResponse { | ||
| 550 | // stack_frames: (*stack_frames).clone(), | ||
| 551 | // total_frames: None, | ||
| 552 | // }) | ||
| 553 | // } | ||
| 554 | // }) | ||
| 555 | // .await; | ||
| 556 | |||
| 557 | // let scopes = vec![ | ||
| 558 | // Scope { | ||
| 559 | // name: "Scope 1".into(), | ||
| 560 | // presentation_hint: None, | ||
| 561 | // variables_reference: 2, | ||
| 562 | // named_variables: None, | ||
| 563 | // indexed_variables: None, | ||
| 564 | // expensive: false, | ||
| 565 | // source: None, | ||
| 566 | // line: None, | ||
| 567 | // column: None, | ||
| 568 | // end_line: None, | ||
| 569 | // end_column: None, | ||
| 570 | // }, | ||
| 571 | // Scope { | ||
| 572 | // name: "Scope 2".into(), | ||
| 573 | // presentation_hint: None, | ||
| 574 | // variables_reference: 4, | ||
| 575 | // named_variables: None, | ||
| 576 | // indexed_variables: None, | ||
| 577 | // expensive: false, | ||
| 578 | // source: None, | ||
| 579 | // line: None, | ||
| 580 | // column: None, | ||
| 581 | // end_line: None, | ||
| 582 | // end_column: None, | ||
| 583 | // }, | ||
| 584 | // ]; | ||
| 585 | |||
| 586 | // client | ||
| 587 | // .on_request::<Scopes, _>({ | ||
| 588 | // let scopes = Arc::new(scopes.clone()); | ||
| 589 | // move |_, args| { | ||
| 590 | // assert_eq!(1, args.frame_id); | ||
| 591 | |||
| 592 | // Ok(dap::ScopesResponse { | ||
| 593 | // scopes: (*scopes).clone(), | ||
| 594 | // }) | ||
| 595 | // } | ||
| 596 | // }) | ||
| 597 | // .await; | ||
| 598 | |||
| 599 | // let scope1_variables = Arc::new(Mutex::new(vec![ | ||
| 600 | // Variable { | ||
| 601 | // name: "variable1".into(), | ||
| 602 | // value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), | ||
| 603 | // type_: None, | ||
| 604 | // presentation_hint: None, | ||
| 605 | // evaluate_name: None, | ||
| 606 | // variables_reference: 3, | ||
| 607 | // named_variables: None, | ||
| 608 | // indexed_variables: None, | ||
| 609 | // memory_reference: None, | ||
| 610 | // declaration_location_reference: None, | ||
| 611 | // value_location_reference: None, | ||
| 612 | // }, | ||
| 613 | // Variable { | ||
| 614 | // name: "variable2".into(), | ||
| 615 | // value: "Value 2".into(), | ||
| 616 | // type_: None, | ||
| 617 | // presentation_hint: None, | ||
| 618 | // evaluate_name: None, | ||
| 619 | // variables_reference: 0, | ||
| 620 | // named_variables: None, | ||
| 621 | // indexed_variables: None, | ||
| 622 | // memory_reference: None, | ||
| 623 | // declaration_location_reference: None, | ||
| 624 | // value_location_reference: None, | ||
| 625 | // }, | ||
| 626 | // ])); | ||
| 627 | |||
| 628 | // let nested_variables = vec![ | ||
| 629 | // Variable { | ||
| 630 | // name: "nested1".into(), | ||
| 631 | // value: "Nested 1".into(), | ||
| 632 | // type_: None, | ||
| 633 | // presentation_hint: None, | ||
| 634 | // evaluate_name: None, | ||
| 635 | // variables_reference: 0, | ||
| 636 | // named_variables: None, | ||
| 637 | // indexed_variables: None, | ||
| 638 | // memory_reference: None, | ||
| 639 | // declaration_location_reference: None, | ||
| 640 | // value_location_reference: None, | ||
| 641 | // }, | ||
| 642 | // Variable { | ||
| 643 | // name: "nested2".into(), | ||
| 644 | // value: "Nested 2".into(), | ||
| 645 | // type_: None, | ||
| 646 | // presentation_hint: None, | ||
| 647 | // evaluate_name: None, | ||
| 648 | // variables_reference: 0, | ||
| 649 | // named_variables: None, | ||
| 650 | // indexed_variables: None, | ||
| 651 | // memory_reference: None, | ||
| 652 | // declaration_location_reference: None, | ||
| 653 | // value_location_reference: None, | ||
| 654 | // }, | ||
| 655 | // ]; | ||
| 656 | |||
| 657 | // let scope2_variables = vec![Variable { | ||
| 658 | // name: "variable3".into(), | ||
| 659 | // value: "Value 3".into(), | ||
| 660 | // type_: None, | ||
| 661 | // presentation_hint: None, | ||
| 662 | // evaluate_name: None, | ||
| 663 | // variables_reference: 0, | ||
| 664 | // named_variables: None, | ||
| 665 | // indexed_variables: None, | ||
| 666 | // memory_reference: None, | ||
| 667 | // declaration_location_reference: None, | ||
| 668 | // value_location_reference: None, | ||
| 669 | // }]; | ||
| 670 | |||
| 671 | // client | ||
| 672 | // .on_request::<Variables, _>({ | ||
| 673 | // let scope1_variables = scope1_variables.clone(); | ||
| 674 | // let nested_variables = Arc::new(nested_variables.clone()); | ||
| 675 | // let scope2_variables = Arc::new(scope2_variables.clone()); | ||
| 676 | // move |_, args| match args.variables_reference { | ||
| 677 | // 4 => Ok(dap::VariablesResponse { | ||
| 678 | // variables: (*scope2_variables).clone(), | ||
| 679 | // }), | ||
| 680 | // 3 => Ok(dap::VariablesResponse { | ||
| 681 | // variables: (*nested_variables).clone(), | ||
| 682 | // }), | ||
| 683 | // 2 => Ok(dap::VariablesResponse { | ||
| 684 | // variables: scope1_variables.lock().unwrap().clone(), | ||
| 685 | // }), | ||
| 686 | // id => unreachable!("unexpected variables reference {id}"), | ||
| 687 | // } | ||
| 688 | // }) | ||
| 689 | // .await; | ||
| 690 | |||
| 691 | // client | ||
| 692 | // .on_request::<Evaluate, _>({ | ||
| 693 | // let called_evaluate = called_evaluate.clone(); | ||
| 694 | // let scope1_variables = scope1_variables.clone(); | ||
| 695 | // move |_, args| { | ||
| 696 | // called_evaluate.store(true, Ordering::SeqCst); | ||
| 697 | |||
| 698 | // assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression); | ||
| 699 | // assert_eq!(Some(1), args.frame_id); | ||
| 700 | // assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context); | ||
| 701 | |||
| 702 | // scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string(); | ||
| 703 | |||
| 704 | // Ok(dap::EvaluateResponse { | ||
| 705 | // result: NEW_VALUE.into(), | ||
| 706 | // type_: None, | ||
| 707 | // presentation_hint: None, | ||
| 708 | // variables_reference: 0, | ||
| 709 | // named_variables: None, | ||
| 710 | // indexed_variables: None, | ||
| 711 | // memory_reference: None, | ||
| 712 | // value_location_reference: None, | ||
| 713 | // }) | ||
| 714 | // } | ||
| 715 | // }) | ||
| 716 | // .await; | ||
| 717 | |||
| 718 | // client | ||
| 719 | // .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 720 | // reason: dap::StoppedEventReason::Pause, | ||
| 721 | // description: None, | ||
| 722 | // thread_id: Some(1), | ||
| 723 | // preserve_focus_hint: None, | ||
| 724 | // text: None, | ||
| 725 | // all_threads_stopped: None, | ||
| 726 | // hit_breakpoint_ids: None, | ||
| 727 | // })) | ||
| 728 | // .await; | ||
| 729 | |||
| 730 | // cx.run_until_parked(); | ||
| 731 | |||
| 732 | // // toggle nested variables for scope 1 | ||
| 733 | // active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| { | ||
| 734 | // debug_panel_item | ||
| 735 | // .mode() | ||
| 736 | // .as_running() | ||
| 737 | // .unwrap() | ||
| 738 | // .update(cx, |running_state, cx| { | ||
| 739 | // running_state | ||
| 740 | // .variable_list() | ||
| 741 | // .update(cx, |variable_list, cx| { | ||
| 742 | // variable_list.toggle_variable( | ||
| 743 | // &VariablePath { | ||
| 744 | // indices: Arc::from([scopes[0].variables_reference]), | ||
| 745 | // }, | ||
| 746 | // cx, | ||
| 747 | // ); | ||
| 748 | // }); | ||
| 749 | // }); | ||
| 750 | // }); | ||
| 751 | |||
| 752 | // cx.run_until_parked(); | ||
| 753 | |||
| 754 | // active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { | ||
| 755 | // debug_panel_item | ||
| 756 | // .mode() | ||
| 757 | // .as_running() | ||
| 758 | // .unwrap() | ||
| 759 | // .update(cx, |running_state, cx| { | ||
| 760 | // running_state.console().update(cx, |console, item_cx| { | ||
| 761 | // console | ||
| 762 | // .query_bar() | ||
| 763 | // .update(item_cx, |query_bar, console_cx| { | ||
| 764 | // query_bar.set_text( | ||
| 765 | // format!("$variable1 = {}", NEW_VALUE), | ||
| 766 | // window, | ||
| 767 | // console_cx, | ||
| 768 | // ); | ||
| 769 | // }); | ||
| 770 | |||
| 771 | // console.evaluate(&menu::Confirm, window, item_cx); | ||
| 772 | // }); | ||
| 773 | // }); | ||
| 774 | // }); | ||
| 775 | |||
| 776 | // cx.run_until_parked(); | ||
| 777 | |||
| 778 | // active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| { | ||
| 779 | // assert_eq!( | ||
| 780 | // "", | ||
| 781 | // debug_panel_item | ||
| 782 | // .mode() | ||
| 783 | // .as_running() | ||
| 784 | // .unwrap() | ||
| 785 | // .read(cx) | ||
| 786 | // .console() | ||
| 787 | // .read(cx) | ||
| 788 | // .query_bar() | ||
| 789 | // .read(cx) | ||
| 790 | // .text(cx) | ||
| 791 | // .as_str() | ||
| 792 | // ); | ||
| 793 | |||
| 794 | // assert_eq!( | ||
| 795 | // format!("{}\n", NEW_VALUE), | ||
| 796 | // debug_panel_item | ||
| 797 | // .mode() | ||
| 798 | // .as_running() | ||
| 799 | // .unwrap() | ||
| 800 | // .read(cx) | ||
| 801 | // .console() | ||
| 802 | // .read(cx) | ||
| 803 | // .editor() | ||
| 804 | // .read(cx) | ||
| 805 | // .text(cx) | ||
| 806 | // .as_str() | ||
| 807 | // ); | ||
| 808 | |||
| 809 | // debug_panel_item | ||
| 810 | // .mode() | ||
| 811 | // .as_running() | ||
| 812 | // .unwrap() | ||
| 813 | // .update(cx, |running_state, cx| { | ||
| 814 | // running_state | ||
| 815 | // .variable_list() | ||
| 816 | // .update(cx, |variable_list, _| { | ||
| 817 | // let scope1_variables = scope1_variables.lock().unwrap().clone(); | ||
| 818 | |||
| 819 | // // scope 1 | ||
| 820 | // // assert_eq!( | ||
| 821 | // // vec![ | ||
| 822 | // // VariableContainer { | ||
| 823 | // // container_reference: scopes[0].variables_reference, | ||
| 824 | // // variable: scope1_variables[0].clone(), | ||
| 825 | // // depth: 1, | ||
| 826 | // // }, | ||
| 827 | // // VariableContainer { | ||
| 828 | // // container_reference: scope1_variables[0].variables_reference, | ||
| 829 | // // variable: nested_variables[0].clone(), | ||
| 830 | // // depth: 2, | ||
| 831 | // // }, | ||
| 832 | // // VariableContainer { | ||
| 833 | // // container_reference: scope1_variables[0].variables_reference, | ||
| 834 | // // variable: nested_variables[1].clone(), | ||
| 835 | // // depth: 2, | ||
| 836 | // // }, | ||
| 837 | // // VariableContainer { | ||
| 838 | // // container_reference: scopes[0].variables_reference, | ||
| 839 | // // variable: scope1_variables[1].clone(), | ||
| 840 | // // depth: 1, | ||
| 841 | // // }, | ||
| 842 | // // ], | ||
| 843 | // // variable_list.variables_by_scope(1, 2).unwrap().variables() | ||
| 844 | // // ); | ||
| 845 | |||
| 846 | // // scope 2 | ||
| 847 | // // assert_eq!( | ||
| 848 | // // vec![VariableContainer { | ||
| 849 | // // container_reference: scopes[1].variables_reference, | ||
| 850 | // // variable: scope2_variables[0].clone(), | ||
| 851 | // // depth: 1, | ||
| 852 | // // }], | ||
| 853 | // // variable_list.variables_by_scope(1, 4).unwrap().variables() | ||
| 854 | // // ); | ||
| 855 | |||
| 856 | // variable_list.assert_visual_entries(vec![ | ||
| 857 | // "v Scope 1", | ||
| 858 | // " v variable1", | ||
| 859 | // " > nested1", | ||
| 860 | // " > nested2", | ||
| 861 | // " > variable2", | ||
| 862 | // ]); | ||
| 863 | |||
| 864 | // // assert visual entries | ||
| 865 | // // assert_eq!( | ||
| 866 | // // vec![ | ||
| 867 | // // VariableListEntry::Scope(scopes[0].clone()), | ||
| 868 | // // VariableListEntry::Variable { | ||
| 869 | // // depth: 1, | ||
| 870 | // // scope: Arc::new(scopes[0].clone()), | ||
| 871 | // // has_children: true, | ||
| 872 | // // variable: Arc::new(scope1_variables[0].clone()), | ||
| 873 | // // container_reference: scopes[0].variables_reference, | ||
| 874 | // // }, | ||
| 875 | // // VariableListEntry::Variable { | ||
| 876 | // // depth: 2, | ||
| 877 | // // scope: Arc::new(scopes[0].clone()), | ||
| 878 | // // has_children: false, | ||
| 879 | // // variable: Arc::new(nested_variables[0].clone()), | ||
| 880 | // // container_reference: scope1_variables[0].variables_reference, | ||
| 881 | // // }, | ||
| 882 | // // VariableListEntry::Variable { | ||
| 883 | // // depth: 2, | ||
| 884 | // // scope: Arc::new(scopes[0].clone()), | ||
| 885 | // // has_children: false, | ||
| 886 | // // variable: Arc::new(nested_variables[1].clone()), | ||
| 887 | // // container_reference: scope1_variables[0].variables_reference, | ||
| 888 | // // }, | ||
| 889 | // // VariableListEntry::Variable { | ||
| 890 | // // depth: 1, | ||
| 891 | // // scope: Arc::new(scopes[0].clone()), | ||
| 892 | // // has_children: false, | ||
| 893 | // // variable: Arc::new(scope1_variables[1].clone()), | ||
| 894 | // // container_reference: scopes[0].variables_reference, | ||
| 895 | // // }, | ||
| 896 | // // VariableListEntry::Scope(scopes[1].clone()), | ||
| 897 | // // ], | ||
| 898 | // // variable_list.entries().get(&1).unwrap().clone() | ||
| 899 | // // ); | ||
| 900 | // }); | ||
| 901 | // }); | ||
| 902 | // }); | ||
| 903 | |||
| 904 | // assert!( | ||
| 905 | // called_evaluate.load(std::sync::atomic::Ordering::SeqCst), | ||
| 906 | // "Expected evaluate request to be called" | ||
| 907 | // ); | ||
| 908 | |||
| 909 | // let shutdown_session = project.update(cx, |project, cx| { | ||
| 910 | // project.dap_store().update(cx, |dap_store, cx| { | ||
| 911 | // dap_store.shutdown_session(&session.read(cx).session_id(), cx) | ||
| 912 | // }) | ||
| 913 | // }); | ||
| 914 | |||
| 915 | // shutdown_session.await.unwrap(); | ||
| 916 | // } | ||
| @@ -0,0 +1,1084 @@ | |||
| 1 | use crate::*; | ||
| 2 | use dap::{ | ||
| 3 | client::SessionId, | ||
| 4 | requests::{ | ||
| 5 | Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace, | ||
| 6 | StartDebugging, StepBack, StepIn, StepOut, Threads, | ||
| 7 | }, | ||
| 8 | DebugRequestType, ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, | ||
| 9 | StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, | ||
| 10 | }; | ||
| 11 | use editor::{ | ||
| 12 | actions::{self}, | ||
| 13 | Editor, EditorMode, MultiBuffer, | ||
| 14 | }; | ||
| 15 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 16 | use project::{ | ||
| 17 | debugger::session::{ThreadId, ThreadStatus}, | ||
| 18 | FakeFs, Project, | ||
| 19 | }; | ||
| 20 | use serde_json::json; | ||
| 21 | use std::{ | ||
| 22 | path::Path, | ||
| 23 | sync::{ | ||
| 24 | atomic::{AtomicBool, Ordering}, | ||
| 25 | Arc, | ||
| 26 | }, | ||
| 27 | }; | ||
| 28 | use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; | ||
| 29 | use tests::{active_debug_session_panel, init_test, init_test_workspace}; | ||
| 30 | use util::path; | ||
| 31 | use workspace::{dock::Panel, Item}; | ||
| 32 | |||
| 33 | #[gpui::test] | ||
| 34 | async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 35 | init_test(cx); | ||
| 36 | |||
| 37 | let fs = FakeFs::new(executor.clone()); | ||
| 38 | |||
| 39 | fs.insert_tree( | ||
| 40 | "/project", | ||
| 41 | json!({ | ||
| 42 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 43 | }), | ||
| 44 | ) | ||
| 45 | .await; | ||
| 46 | |||
| 47 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 48 | let workspace = init_test_workspace(&project, cx).await; | ||
| 49 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 50 | |||
| 51 | let task = project.update(cx, |project, cx| { | ||
| 52 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 53 | }); | ||
| 54 | |||
| 55 | let session = task.await.unwrap(); | ||
| 56 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 57 | |||
| 58 | client | ||
| 59 | .on_request::<Threads, _>(move |_, _| { | ||
| 60 | Ok(dap::ThreadsResponse { | ||
| 61 | threads: vec![dap::Thread { | ||
| 62 | id: 1, | ||
| 63 | name: "Thread 1".into(), | ||
| 64 | }], | ||
| 65 | }) | ||
| 66 | }) | ||
| 67 | .await; | ||
| 68 | |||
| 69 | client | ||
| 70 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 71 | Ok(dap::StackTraceResponse { | ||
| 72 | stack_frames: Vec::default(), | ||
| 73 | total_frames: None, | ||
| 74 | }) | ||
| 75 | }) | ||
| 76 | .await; | ||
| 77 | |||
| 78 | // assert we have a debug panel item before the session has stopped | ||
| 79 | workspace | ||
| 80 | .update(cx, |workspace, _window, cx| { | ||
| 81 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 82 | let active_session = debug_panel.update(cx, |debug_panel, cx| { | ||
| 83 | debug_panel.active_session(cx).unwrap() | ||
| 84 | }); | ||
| 85 | |||
| 86 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 87 | active_session | ||
| 88 | .mode() | ||
| 89 | .as_running() | ||
| 90 | .expect("Session should be running by this point") | ||
| 91 | .clone() | ||
| 92 | }); | ||
| 93 | |||
| 94 | debug_panel.update(cx, |this, cx| { | ||
| 95 | assert!(this.active_session(cx).is_some()); | ||
| 96 | // we have one active session and one inert item | ||
| 97 | assert_eq!(2, this.pane().unwrap().read(cx).items_len()); | ||
| 98 | assert!(running_state.read(cx).selected_thread_id().is_none()); | ||
| 99 | }); | ||
| 100 | }) | ||
| 101 | .unwrap(); | ||
| 102 | |||
| 103 | client | ||
| 104 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 105 | reason: dap::StoppedEventReason::Pause, | ||
| 106 | description: None, | ||
| 107 | thread_id: Some(1), | ||
| 108 | preserve_focus_hint: None, | ||
| 109 | text: None, | ||
| 110 | all_threads_stopped: None, | ||
| 111 | hit_breakpoint_ids: None, | ||
| 112 | })) | ||
| 113 | .await; | ||
| 114 | |||
| 115 | cx.run_until_parked(); | ||
| 116 | |||
| 117 | workspace | ||
| 118 | .update(cx, |workspace, _window, cx| { | ||
| 119 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 120 | let active_session = debug_panel | ||
| 121 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 122 | .unwrap(); | ||
| 123 | |||
| 124 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 125 | active_session | ||
| 126 | .mode() | ||
| 127 | .as_running() | ||
| 128 | .expect("Session should be running by this point") | ||
| 129 | .clone() | ||
| 130 | }); | ||
| 131 | |||
| 132 | // we have one active session and one inert item | ||
| 133 | assert_eq!( | ||
| 134 | 2, | ||
| 135 | debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 136 | ); | ||
| 137 | assert_eq!(client.id(), running_state.read(cx).session_id()); | ||
| 138 | assert_eq!( | ||
| 139 | ThreadId(1), | ||
| 140 | running_state.read(cx).selected_thread_id().unwrap() | ||
| 141 | ); | ||
| 142 | }) | ||
| 143 | .unwrap(); | ||
| 144 | |||
| 145 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 146 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 147 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 148 | }) | ||
| 149 | }); | ||
| 150 | |||
| 151 | shutdown_session.await.unwrap(); | ||
| 152 | |||
| 153 | // assert we still have a debug panel item after the client shutdown | ||
| 154 | workspace | ||
| 155 | .update(cx, |workspace, _window, cx| { | ||
| 156 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 157 | |||
| 158 | let active_session = debug_panel | ||
| 159 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 160 | .unwrap(); | ||
| 161 | |||
| 162 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 163 | active_session | ||
| 164 | .mode() | ||
| 165 | .as_running() | ||
| 166 | .expect("Session should be running by this point") | ||
| 167 | .clone() | ||
| 168 | }); | ||
| 169 | |||
| 170 | debug_panel.update(cx, |this, cx| { | ||
| 171 | assert!(this.active_session(cx).is_some()); | ||
| 172 | assert_eq!(2, this.pane().unwrap().read(cx).items_len()); | ||
| 173 | assert_eq!( | ||
| 174 | ThreadId(1), | ||
| 175 | running_state.read(cx).selected_thread_id().unwrap() | ||
| 176 | ); | ||
| 177 | }); | ||
| 178 | }) | ||
| 179 | .unwrap(); | ||
| 180 | } | ||
| 181 | |||
| 182 | #[gpui::test] | ||
| 183 | async fn test_we_can_only_have_one_panel_per_debug_session( | ||
| 184 | executor: BackgroundExecutor, | ||
| 185 | cx: &mut TestAppContext, | ||
| 186 | ) { | ||
| 187 | init_test(cx); | ||
| 188 | |||
| 189 | let fs = FakeFs::new(executor.clone()); | ||
| 190 | |||
| 191 | fs.insert_tree( | ||
| 192 | "/project", | ||
| 193 | json!({ | ||
| 194 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 195 | }), | ||
| 196 | ) | ||
| 197 | .await; | ||
| 198 | |||
| 199 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 200 | let workspace = init_test_workspace(&project, cx).await; | ||
| 201 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 202 | |||
| 203 | let task = project.update(cx, |project, cx| { | ||
| 204 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 205 | }); | ||
| 206 | |||
| 207 | let session = task.await.unwrap(); | ||
| 208 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 209 | |||
| 210 | client | ||
| 211 | .on_request::<Threads, _>(move |_, _| { | ||
| 212 | Ok(dap::ThreadsResponse { | ||
| 213 | threads: vec![dap::Thread { | ||
| 214 | id: 1, | ||
| 215 | name: "Thread 1".into(), | ||
| 216 | }], | ||
| 217 | }) | ||
| 218 | }) | ||
| 219 | .await; | ||
| 220 | |||
| 221 | client | ||
| 222 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 223 | Ok(dap::StackTraceResponse { | ||
| 224 | stack_frames: Vec::default(), | ||
| 225 | total_frames: None, | ||
| 226 | }) | ||
| 227 | }) | ||
| 228 | .await; | ||
| 229 | |||
| 230 | // assert we have a debug panel item before the session has stopped | ||
| 231 | workspace | ||
| 232 | .update(cx, |workspace, _window, cx| { | ||
| 233 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 234 | |||
| 235 | debug_panel.update(cx, |this, cx| { | ||
| 236 | assert!(this.active_session(cx).is_some()); | ||
| 237 | // we have one active session and one inert item | ||
| 238 | assert_eq!(2, this.pane().unwrap().read(cx).items_len()); | ||
| 239 | }); | ||
| 240 | }) | ||
| 241 | .unwrap(); | ||
| 242 | |||
| 243 | client | ||
| 244 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 245 | reason: dap::StoppedEventReason::Pause, | ||
| 246 | description: None, | ||
| 247 | thread_id: Some(1), | ||
| 248 | preserve_focus_hint: None, | ||
| 249 | text: None, | ||
| 250 | all_threads_stopped: None, | ||
| 251 | hit_breakpoint_ids: None, | ||
| 252 | })) | ||
| 253 | .await; | ||
| 254 | |||
| 255 | cx.run_until_parked(); | ||
| 256 | |||
| 257 | // assert we added a debug panel item | ||
| 258 | workspace | ||
| 259 | .update(cx, |workspace, _window, cx| { | ||
| 260 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 261 | let active_session = debug_panel | ||
| 262 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 263 | .unwrap(); | ||
| 264 | |||
| 265 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 266 | active_session | ||
| 267 | .mode() | ||
| 268 | .as_running() | ||
| 269 | .expect("Session should be running by this point") | ||
| 270 | .clone() | ||
| 271 | }); | ||
| 272 | |||
| 273 | // we have one active session and one inert item | ||
| 274 | assert_eq!( | ||
| 275 | 2, | ||
| 276 | debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 277 | ); | ||
| 278 | assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap()); | ||
| 279 | assert_eq!( | ||
| 280 | ThreadId(1), | ||
| 281 | running_state.read(cx).selected_thread_id().unwrap() | ||
| 282 | ); | ||
| 283 | }) | ||
| 284 | .unwrap(); | ||
| 285 | |||
| 286 | client | ||
| 287 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 288 | reason: dap::StoppedEventReason::Pause, | ||
| 289 | description: None, | ||
| 290 | thread_id: Some(2), | ||
| 291 | preserve_focus_hint: None, | ||
| 292 | text: None, | ||
| 293 | all_threads_stopped: None, | ||
| 294 | hit_breakpoint_ids: None, | ||
| 295 | })) | ||
| 296 | .await; | ||
| 297 | |||
| 298 | cx.run_until_parked(); | ||
| 299 | |||
| 300 | workspace | ||
| 301 | .update(cx, |workspace, _window, cx| { | ||
| 302 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 303 | let active_session = debug_panel | ||
| 304 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 305 | .unwrap(); | ||
| 306 | |||
| 307 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 308 | active_session | ||
| 309 | .mode() | ||
| 310 | .as_running() | ||
| 311 | .expect("Session should be running by this point") | ||
| 312 | .clone() | ||
| 313 | }); | ||
| 314 | |||
| 315 | // we have one active session and one inert item | ||
| 316 | assert_eq!( | ||
| 317 | 2, | ||
| 318 | debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) | ||
| 319 | ); | ||
| 320 | assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap()); | ||
| 321 | assert_eq!( | ||
| 322 | ThreadId(1), | ||
| 323 | running_state.read(cx).selected_thread_id().unwrap() | ||
| 324 | ); | ||
| 325 | }) | ||
| 326 | .unwrap(); | ||
| 327 | |||
| 328 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 329 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 330 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 331 | }) | ||
| 332 | }); | ||
| 333 | |||
| 334 | shutdown_session.await.unwrap(); | ||
| 335 | |||
| 336 | // assert we still have a debug panel item after the client shutdown | ||
| 337 | workspace | ||
| 338 | .update(cx, |workspace, _window, cx| { | ||
| 339 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 340 | let active_session = debug_panel | ||
| 341 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 342 | .unwrap(); | ||
| 343 | |||
| 344 | let running_state = active_session.update(cx, |active_session, _| { | ||
| 345 | active_session | ||
| 346 | .mode() | ||
| 347 | .as_running() | ||
| 348 | .expect("Session should be running by this point") | ||
| 349 | .clone() | ||
| 350 | }); | ||
| 351 | |||
| 352 | debug_panel.update(cx, |this, cx| { | ||
| 353 | assert!(this.active_session(cx).is_some()); | ||
| 354 | assert_eq!(2, this.pane().unwrap().read(cx).items_len()); | ||
| 355 | assert_eq!( | ||
| 356 | ThreadId(1), | ||
| 357 | running_state.read(cx).selected_thread_id().unwrap() | ||
| 358 | ); | ||
| 359 | }); | ||
| 360 | }) | ||
| 361 | .unwrap(); | ||
| 362 | } | ||
| 363 | |||
| 364 | #[gpui::test] | ||
| 365 | async fn test_handle_successful_run_in_terminal_reverse_request( | ||
| 366 | executor: BackgroundExecutor, | ||
| 367 | cx: &mut TestAppContext, | ||
| 368 | ) { | ||
| 369 | init_test(cx); | ||
| 370 | |||
| 371 | let send_response = Arc::new(AtomicBool::new(false)); | ||
| 372 | |||
| 373 | let fs = FakeFs::new(executor.clone()); | ||
| 374 | |||
| 375 | fs.insert_tree( | ||
| 376 | "/project", | ||
| 377 | json!({ | ||
| 378 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 379 | }), | ||
| 380 | ) | ||
| 381 | .await; | ||
| 382 | |||
| 383 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 384 | let workspace = init_test_workspace(&project, cx).await; | ||
| 385 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 386 | |||
| 387 | let task = project.update(cx, |project, cx| { | ||
| 388 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 389 | }); | ||
| 390 | |||
| 391 | let session = task.await.unwrap(); | ||
| 392 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 393 | |||
| 394 | client | ||
| 395 | .on_response::<RunInTerminal, _>({ | ||
| 396 | let send_response = send_response.clone(); | ||
| 397 | move |response| { | ||
| 398 | send_response.store(true, Ordering::SeqCst); | ||
| 399 | |||
| 400 | assert!(response.success); | ||
| 401 | assert!(response.body.is_some()); | ||
| 402 | } | ||
| 403 | }) | ||
| 404 | .await; | ||
| 405 | |||
| 406 | client | ||
| 407 | .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments { | ||
| 408 | kind: None, | ||
| 409 | title: None, | ||
| 410 | cwd: std::env::temp_dir().to_string_lossy().to_string(), | ||
| 411 | args: vec![], | ||
| 412 | env: None, | ||
| 413 | args_can_be_interpreted_by_shell: None, | ||
| 414 | }) | ||
| 415 | .await; | ||
| 416 | |||
| 417 | cx.run_until_parked(); | ||
| 418 | |||
| 419 | assert!( | ||
| 420 | send_response.load(std::sync::atomic::Ordering::SeqCst), | ||
| 421 | "Expected to receive response from reverse request" | ||
| 422 | ); | ||
| 423 | |||
| 424 | workspace | ||
| 425 | .update(cx, |workspace, _window, cx| { | ||
| 426 | let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap(); | ||
| 427 | |||
| 428 | let panel = terminal_panel.read(cx).pane().unwrap().read(cx); | ||
| 429 | |||
| 430 | assert_eq!(1, panel.items_len()); | ||
| 431 | assert!(panel | ||
| 432 | .active_item() | ||
| 433 | .unwrap() | ||
| 434 | .downcast::<TerminalView>() | ||
| 435 | .unwrap() | ||
| 436 | .read(cx) | ||
| 437 | .terminal() | ||
| 438 | .read(cx) | ||
| 439 | .debug_terminal()); | ||
| 440 | }) | ||
| 441 | .unwrap(); | ||
| 442 | |||
| 443 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 444 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 445 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 446 | }) | ||
| 447 | }); | ||
| 448 | |||
| 449 | shutdown_session.await.unwrap(); | ||
| 450 | } | ||
| 451 | |||
| 452 | // // covers that we always send a response back, if something when wrong, | ||
| 453 | // // while spawning the terminal | ||
| 454 | #[gpui::test] | ||
| 455 | async fn test_handle_error_run_in_terminal_reverse_request( | ||
| 456 | executor: BackgroundExecutor, | ||
| 457 | cx: &mut TestAppContext, | ||
| 458 | ) { | ||
| 459 | init_test(cx); | ||
| 460 | |||
| 461 | let send_response = Arc::new(AtomicBool::new(false)); | ||
| 462 | |||
| 463 | let fs = FakeFs::new(executor.clone()); | ||
| 464 | |||
| 465 | fs.insert_tree( | ||
| 466 | "/project", | ||
| 467 | json!({ | ||
| 468 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 469 | }), | ||
| 470 | ) | ||
| 471 | .await; | ||
| 472 | |||
| 473 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 474 | let workspace = init_test_workspace(&project, cx).await; | ||
| 475 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 476 | |||
| 477 | let task = project.update(cx, |project, cx| { | ||
| 478 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 479 | }); | ||
| 480 | |||
| 481 | let session = task.await.unwrap(); | ||
| 482 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 483 | |||
| 484 | client | ||
| 485 | .on_response::<RunInTerminal, _>({ | ||
| 486 | let send_response = send_response.clone(); | ||
| 487 | move |response| { | ||
| 488 | send_response.store(true, Ordering::SeqCst); | ||
| 489 | |||
| 490 | assert!(!response.success); | ||
| 491 | assert!(response.body.is_some()); | ||
| 492 | } | ||
| 493 | }) | ||
| 494 | .await; | ||
| 495 | |||
| 496 | client | ||
| 497 | .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments { | ||
| 498 | kind: None, | ||
| 499 | title: None, | ||
| 500 | cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail | ||
| 501 | args: vec![], | ||
| 502 | env: None, | ||
| 503 | args_can_be_interpreted_by_shell: None, | ||
| 504 | }) | ||
| 505 | .await; | ||
| 506 | |||
| 507 | cx.run_until_parked(); | ||
| 508 | |||
| 509 | assert!( | ||
| 510 | send_response.load(std::sync::atomic::Ordering::SeqCst), | ||
| 511 | "Expected to receive response from reverse request" | ||
| 512 | ); | ||
| 513 | |||
| 514 | workspace | ||
| 515 | .update(cx, |workspace, _window, cx| { | ||
| 516 | let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap(); | ||
| 517 | |||
| 518 | assert_eq!( | ||
| 519 | 0, | ||
| 520 | terminal_panel.read(cx).pane().unwrap().read(cx).items_len() | ||
| 521 | ); | ||
| 522 | }) | ||
| 523 | .unwrap(); | ||
| 524 | |||
| 525 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 526 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 527 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 528 | }) | ||
| 529 | }); | ||
| 530 | |||
| 531 | shutdown_session.await.unwrap(); | ||
| 532 | } | ||
| 533 | |||
| 534 | #[gpui::test] | ||
| 535 | async fn test_handle_start_debugging_reverse_request( | ||
| 536 | executor: BackgroundExecutor, | ||
| 537 | cx: &mut TestAppContext, | ||
| 538 | ) { | ||
| 539 | init_test(cx); | ||
| 540 | |||
| 541 | let send_response = Arc::new(AtomicBool::new(false)); | ||
| 542 | |||
| 543 | let fs = FakeFs::new(executor.clone()); | ||
| 544 | |||
| 545 | fs.insert_tree( | ||
| 546 | "/project", | ||
| 547 | json!({ | ||
| 548 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 549 | }), | ||
| 550 | ) | ||
| 551 | .await; | ||
| 552 | |||
| 553 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 554 | let workspace = init_test_workspace(&project, cx).await; | ||
| 555 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 556 | |||
| 557 | let task = project.update(cx, |project, cx| { | ||
| 558 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 559 | }); | ||
| 560 | |||
| 561 | let session = task.await.unwrap(); | ||
| 562 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 563 | |||
| 564 | client | ||
| 565 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 566 | Ok(dap::ThreadsResponse { | ||
| 567 | threads: vec![dap::Thread { | ||
| 568 | id: 1, | ||
| 569 | name: "Thread 1".into(), | ||
| 570 | }], | ||
| 571 | }) | ||
| 572 | }) | ||
| 573 | .await; | ||
| 574 | |||
| 575 | client | ||
| 576 | .on_response::<StartDebugging, _>({ | ||
| 577 | let send_response = send_response.clone(); | ||
| 578 | move |response| { | ||
| 579 | send_response.store(true, Ordering::SeqCst); | ||
| 580 | |||
| 581 | assert!(response.success); | ||
| 582 | assert!(response.body.is_some()); | ||
| 583 | } | ||
| 584 | }) | ||
| 585 | .await; | ||
| 586 | |||
| 587 | client | ||
| 588 | .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments { | ||
| 589 | configuration: json!({}), | ||
| 590 | request: StartDebuggingRequestArgumentsRequest::Launch, | ||
| 591 | }) | ||
| 592 | .await; | ||
| 593 | |||
| 594 | cx.run_until_parked(); | ||
| 595 | |||
| 596 | let child_session = project.update(cx, |project, cx| { | ||
| 597 | project | ||
| 598 | .dap_store() | ||
| 599 | .read(cx) | ||
| 600 | .session_by_id(SessionId(1)) | ||
| 601 | .unwrap() | ||
| 602 | }); | ||
| 603 | let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 604 | |||
| 605 | client | ||
| 606 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 607 | Ok(dap::ThreadsResponse { | ||
| 608 | threads: vec![dap::Thread { | ||
| 609 | id: 1, | ||
| 610 | name: "Thread 1".into(), | ||
| 611 | }], | ||
| 612 | }) | ||
| 613 | }) | ||
| 614 | .await; | ||
| 615 | |||
| 616 | child_client | ||
| 617 | .on_request::<Disconnect, _>(move |_, _| Ok(())) | ||
| 618 | .await; | ||
| 619 | |||
| 620 | child_client | ||
| 621 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 622 | reason: dap::StoppedEventReason::Pause, | ||
| 623 | description: None, | ||
| 624 | thread_id: Some(2), | ||
| 625 | preserve_focus_hint: None, | ||
| 626 | text: None, | ||
| 627 | all_threads_stopped: None, | ||
| 628 | hit_breakpoint_ids: None, | ||
| 629 | })) | ||
| 630 | .await; | ||
| 631 | |||
| 632 | cx.run_until_parked(); | ||
| 633 | |||
| 634 | assert!( | ||
| 635 | send_response.load(std::sync::atomic::Ordering::SeqCst), | ||
| 636 | "Expected to receive response from reverse request" | ||
| 637 | ); | ||
| 638 | |||
| 639 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 640 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 641 | dap_store.shutdown_session(child_session.read(cx).session_id(), cx) | ||
| 642 | }) | ||
| 643 | }); | ||
| 644 | |||
| 645 | shutdown_session.await.unwrap(); | ||
| 646 | } | ||
| 647 | |||
| 648 | #[gpui::test] | ||
| 649 | async fn test_debug_panel_item_thread_status_reset_on_failure( | ||
| 650 | executor: BackgroundExecutor, | ||
| 651 | cx: &mut TestAppContext, | ||
| 652 | ) { | ||
| 653 | init_test(cx); | ||
| 654 | |||
| 655 | let fs = FakeFs::new(executor.clone()); | ||
| 656 | |||
| 657 | fs.insert_tree( | ||
| 658 | "/project", | ||
| 659 | json!({ | ||
| 660 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 661 | }), | ||
| 662 | ) | ||
| 663 | .await; | ||
| 664 | |||
| 665 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 666 | let workspace = init_test_workspace(&project, cx).await; | ||
| 667 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 668 | |||
| 669 | let task = project.update(cx, |project, cx| { | ||
| 670 | project.start_debug_session( | ||
| 671 | dap::test_config( | ||
| 672 | DebugRequestType::Launch, | ||
| 673 | None, | ||
| 674 | Some(dap::Capabilities { | ||
| 675 | supports_step_back: Some(true), | ||
| 676 | ..Default::default() | ||
| 677 | }), | ||
| 678 | ), | ||
| 679 | cx, | ||
| 680 | ) | ||
| 681 | }); | ||
| 682 | |||
| 683 | let session = task.await.unwrap(); | ||
| 684 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 685 | const THREAD_ID_NUM: u64 = 1; | ||
| 686 | |||
| 687 | client | ||
| 688 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 689 | Ok(dap::ThreadsResponse { | ||
| 690 | threads: vec![dap::Thread { | ||
| 691 | id: THREAD_ID_NUM, | ||
| 692 | name: "Thread 1".into(), | ||
| 693 | }], | ||
| 694 | }) | ||
| 695 | }) | ||
| 696 | .await; | ||
| 697 | |||
| 698 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 699 | |||
| 700 | client | ||
| 701 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 702 | Ok(dap::StackTraceResponse { | ||
| 703 | stack_frames: Vec::default(), | ||
| 704 | total_frames: None, | ||
| 705 | }) | ||
| 706 | }) | ||
| 707 | .await; | ||
| 708 | |||
| 709 | client | ||
| 710 | .on_request::<Next, _>(move |_, _| { | ||
| 711 | Err(ErrorResponse { | ||
| 712 | error: Some(dap::Message { | ||
| 713 | id: 1, | ||
| 714 | format: "error".into(), | ||
| 715 | variables: None, | ||
| 716 | send_telemetry: None, | ||
| 717 | show_user: None, | ||
| 718 | url: None, | ||
| 719 | url_label: None, | ||
| 720 | }), | ||
| 721 | }) | ||
| 722 | }) | ||
| 723 | .await; | ||
| 724 | |||
| 725 | client | ||
| 726 | .on_request::<StepOut, _>(move |_, _| { | ||
| 727 | Err(ErrorResponse { | ||
| 728 | error: Some(dap::Message { | ||
| 729 | id: 1, | ||
| 730 | format: "error".into(), | ||
| 731 | variables: None, | ||
| 732 | send_telemetry: None, | ||
| 733 | show_user: None, | ||
| 734 | url: None, | ||
| 735 | url_label: None, | ||
| 736 | }), | ||
| 737 | }) | ||
| 738 | }) | ||
| 739 | .await; | ||
| 740 | |||
| 741 | client | ||
| 742 | .on_request::<StepIn, _>(move |_, _| { | ||
| 743 | Err(ErrorResponse { | ||
| 744 | error: Some(dap::Message { | ||
| 745 | id: 1, | ||
| 746 | format: "error".into(), | ||
| 747 | variables: None, | ||
| 748 | send_telemetry: None, | ||
| 749 | show_user: None, | ||
| 750 | url: None, | ||
| 751 | url_label: None, | ||
| 752 | }), | ||
| 753 | }) | ||
| 754 | }) | ||
| 755 | .await; | ||
| 756 | |||
| 757 | client | ||
| 758 | .on_request::<StepBack, _>(move |_, _| { | ||
| 759 | Err(ErrorResponse { | ||
| 760 | error: Some(dap::Message { | ||
| 761 | id: 1, | ||
| 762 | format: "error".into(), | ||
| 763 | variables: None, | ||
| 764 | send_telemetry: None, | ||
| 765 | show_user: None, | ||
| 766 | url: None, | ||
| 767 | url_label: None, | ||
| 768 | }), | ||
| 769 | }) | ||
| 770 | }) | ||
| 771 | .await; | ||
| 772 | |||
| 773 | client | ||
| 774 | .on_request::<Continue, _>(move |_, _| { | ||
| 775 | Err(ErrorResponse { | ||
| 776 | error: Some(dap::Message { | ||
| 777 | id: 1, | ||
| 778 | format: "error".into(), | ||
| 779 | variables: None, | ||
| 780 | send_telemetry: None, | ||
| 781 | show_user: None, | ||
| 782 | url: None, | ||
| 783 | url_label: None, | ||
| 784 | }), | ||
| 785 | }) | ||
| 786 | }) | ||
| 787 | .await; | ||
| 788 | |||
| 789 | client | ||
| 790 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 791 | reason: dap::StoppedEventReason::Pause, | ||
| 792 | description: None, | ||
| 793 | thread_id: Some(1), | ||
| 794 | preserve_focus_hint: None, | ||
| 795 | text: None, | ||
| 796 | all_threads_stopped: None, | ||
| 797 | hit_breakpoint_ids: None, | ||
| 798 | })) | ||
| 799 | .await; | ||
| 800 | |||
| 801 | let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| { | ||
| 802 | item.mode() | ||
| 803 | .as_running() | ||
| 804 | .expect("Session should be running by this point") | ||
| 805 | .clone() | ||
| 806 | }); | ||
| 807 | |||
| 808 | cx.run_until_parked(); | ||
| 809 | let thread_id = ThreadId(1); | ||
| 810 | |||
| 811 | for operation in &[ | ||
| 812 | "step_over", | ||
| 813 | "continue_thread", | ||
| 814 | "step_back", | ||
| 815 | "step_in", | ||
| 816 | "step_out", | ||
| 817 | ] { | ||
| 818 | running_state.update(cx, |running_state, cx| match *operation { | ||
| 819 | "step_over" => running_state.step_over(cx), | ||
| 820 | "continue_thread" => running_state.continue_thread(cx), | ||
| 821 | "step_back" => running_state.step_back(cx), | ||
| 822 | "step_in" => running_state.step_in(cx), | ||
| 823 | "step_out" => running_state.step_out(cx), | ||
| 824 | _ => unreachable!(), | ||
| 825 | }); | ||
| 826 | |||
| 827 | // Check that we step the thread status to the correct intermediate state | ||
| 828 | running_state.update(cx, |running_state, cx| { | ||
| 829 | assert_eq!( | ||
| 830 | running_state | ||
| 831 | .thread_status(cx) | ||
| 832 | .expect("There should be an active thread selected"), | ||
| 833 | match *operation { | ||
| 834 | "continue_thread" => ThreadStatus::Running, | ||
| 835 | _ => ThreadStatus::Stepping, | ||
| 836 | }, | ||
| 837 | "Thread status was not set to correct intermediate state after {} request", | ||
| 838 | operation | ||
| 839 | ); | ||
| 840 | }); | ||
| 841 | |||
| 842 | cx.run_until_parked(); | ||
| 843 | |||
| 844 | running_state.update(cx, |running_state, cx| { | ||
| 845 | assert_eq!( | ||
| 846 | running_state | ||
| 847 | .thread_status(cx) | ||
| 848 | .expect("There should be an active thread selected"), | ||
| 849 | ThreadStatus::Stopped, | ||
| 850 | "Thread status not reset to Stopped after failed {}", | ||
| 851 | operation | ||
| 852 | ); | ||
| 853 | |||
| 854 | // update state to running, so we can test it actually changes the status back to stopped | ||
| 855 | running_state | ||
| 856 | .session() | ||
| 857 | .update(cx, |session, cx| session.continue_thread(thread_id, cx)); | ||
| 858 | }); | ||
| 859 | } | ||
| 860 | |||
| 861 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 862 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 863 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 864 | }) | ||
| 865 | }); | ||
| 866 | |||
| 867 | shutdown_session.await.unwrap(); | ||
| 868 | } | ||
| 869 | |||
| 870 | #[gpui::test] | ||
| 871 | async fn test_send_breakpoints_when_editor_has_been_saved( | ||
| 872 | executor: BackgroundExecutor, | ||
| 873 | cx: &mut TestAppContext, | ||
| 874 | ) { | ||
| 875 | init_test(cx); | ||
| 876 | |||
| 877 | let fs = FakeFs::new(executor.clone()); | ||
| 878 | |||
| 879 | fs.insert_tree( | ||
| 880 | path!("/project"), | ||
| 881 | json!({ | ||
| 882 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 883 | }), | ||
| 884 | ) | ||
| 885 | .await; | ||
| 886 | |||
| 887 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 888 | let workspace = init_test_workspace(&project, cx).await; | ||
| 889 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 890 | let project_path = Path::new(path!("/project")); | ||
| 891 | let worktree = project | ||
| 892 | .update(cx, |project, cx| project.find_worktree(project_path, cx)) | ||
| 893 | .expect("This worktree should exist in project") | ||
| 894 | .0; | ||
| 895 | |||
| 896 | let worktree_id = workspace | ||
| 897 | .update(cx, |_, _, cx| worktree.read(cx).id()) | ||
| 898 | .unwrap(); | ||
| 899 | |||
| 900 | let task = project.update(cx, |project, cx| { | ||
| 901 | project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) | ||
| 902 | }); | ||
| 903 | |||
| 904 | let session = task.await.unwrap(); | ||
| 905 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 906 | |||
| 907 | let buffer = project | ||
| 908 | .update(cx, |project, cx| { | ||
| 909 | project.open_buffer((worktree_id, "main.rs"), cx) | ||
| 910 | }) | ||
| 911 | .await | ||
| 912 | .unwrap(); | ||
| 913 | |||
| 914 | let (editor, cx) = cx.add_window_view(|window, cx| { | ||
| 915 | Editor::new( | ||
| 916 | EditorMode::Full, | ||
| 917 | MultiBuffer::build_from_buffer(buffer, cx), | ||
| 918 | Some(project.clone()), | ||
| 919 | window, | ||
| 920 | cx, | ||
| 921 | ) | ||
| 922 | }); | ||
| 923 | |||
| 924 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 925 | |||
| 926 | client | ||
| 927 | .on_request::<StackTrace, _>(move |_, _| { | ||
| 928 | Ok(dap::StackTraceResponse { | ||
| 929 | stack_frames: Vec::default(), | ||
| 930 | total_frames: None, | ||
| 931 | }) | ||
| 932 | }) | ||
| 933 | .await; | ||
| 934 | |||
| 935 | client | ||
| 936 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 937 | reason: dap::StoppedEventReason::Pause, | ||
| 938 | description: None, | ||
| 939 | thread_id: Some(1), | ||
| 940 | preserve_focus_hint: None, | ||
| 941 | text: None, | ||
| 942 | all_threads_stopped: None, | ||
| 943 | hit_breakpoint_ids: None, | ||
| 944 | })) | ||
| 945 | .await; | ||
| 946 | |||
| 947 | let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 948 | client | ||
| 949 | .on_request::<SetBreakpoints, _>({ | ||
| 950 | let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 951 | move |_, args| { | ||
| 952 | assert_eq!(path!("/project/main.rs"), args.source.path.unwrap()); | ||
| 953 | assert_eq!( | ||
| 954 | vec![SourceBreakpoint { | ||
| 955 | line: 2, | ||
| 956 | column: None, | ||
| 957 | condition: None, | ||
| 958 | hit_condition: None, | ||
| 959 | log_message: None, | ||
| 960 | mode: None | ||
| 961 | }], | ||
| 962 | args.breakpoints.unwrap() | ||
| 963 | ); | ||
| 964 | assert!(!args.source_modified.unwrap()); | ||
| 965 | |||
| 966 | called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 967 | |||
| 968 | Ok(dap::SetBreakpointsResponse { | ||
| 969 | breakpoints: Vec::default(), | ||
| 970 | }) | ||
| 971 | } | ||
| 972 | }) | ||
| 973 | .await; | ||
| 974 | |||
| 975 | editor.update_in(cx, |editor, window, cx| { | ||
| 976 | editor.move_down(&actions::MoveDown, window, cx); | ||
| 977 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 978 | }); | ||
| 979 | |||
| 980 | cx.run_until_parked(); | ||
| 981 | |||
| 982 | assert!( | ||
| 983 | called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 984 | "SetBreakpoint request must be called" | ||
| 985 | ); | ||
| 986 | |||
| 987 | let called_set_breakpoints = Arc::new(AtomicBool::new(false)); | ||
| 988 | client | ||
| 989 | .on_request::<SetBreakpoints, _>({ | ||
| 990 | let called_set_breakpoints = called_set_breakpoints.clone(); | ||
| 991 | move |_, args| { | ||
| 992 | assert_eq!(path!("/project/main.rs"), args.source.path.unwrap()); | ||
| 993 | assert_eq!( | ||
| 994 | vec![SourceBreakpoint { | ||
| 995 | line: 3, | ||
| 996 | column: None, | ||
| 997 | condition: None, | ||
| 998 | hit_condition: None, | ||
| 999 | log_message: None, | ||
| 1000 | mode: None | ||
| 1001 | }], | ||
| 1002 | args.breakpoints.unwrap() | ||
| 1003 | ); | ||
| 1004 | assert!(args.source_modified.unwrap()); | ||
| 1005 | |||
| 1006 | called_set_breakpoints.store(true, Ordering::SeqCst); | ||
| 1007 | |||
| 1008 | Ok(dap::SetBreakpointsResponse { | ||
| 1009 | breakpoints: Vec::default(), | ||
| 1010 | }) | ||
| 1011 | } | ||
| 1012 | }) | ||
| 1013 | .await; | ||
| 1014 | |||
| 1015 | editor.update_in(cx, |editor, window, cx| { | ||
| 1016 | editor.move_up(&actions::MoveUp, window, cx); | ||
| 1017 | editor.insert("new text\n", window, cx); | ||
| 1018 | }); | ||
| 1019 | |||
| 1020 | editor | ||
| 1021 | .update_in(cx, |editor, window, cx| { | ||
| 1022 | editor.save(true, project.clone(), window, cx) | ||
| 1023 | }) | ||
| 1024 | .await | ||
| 1025 | .unwrap(); | ||
| 1026 | |||
| 1027 | cx.run_until_parked(); | ||
| 1028 | |||
| 1029 | assert!( | ||
| 1030 | called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1031 | "SetBreakpoint request must be called after editor is saved" | ||
| 1032 | ); | ||
| 1033 | |||
| 1034 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 1035 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 1036 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 1037 | }) | ||
| 1038 | }); | ||
| 1039 | |||
| 1040 | shutdown_session.await.unwrap(); | ||
| 1041 | } | ||
| 1042 | |||
| 1043 | #[gpui::test] | ||
| 1044 | async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( | ||
| 1045 | executor: BackgroundExecutor, | ||
| 1046 | cx: &mut TestAppContext, | ||
| 1047 | ) { | ||
| 1048 | init_test(cx); | ||
| 1049 | |||
| 1050 | let fs = FakeFs::new(executor.clone()); | ||
| 1051 | |||
| 1052 | fs.insert_tree( | ||
| 1053 | "/project", | ||
| 1054 | json!({ | ||
| 1055 | "main.rs": "First line\nSecond line\nThird line\nFourth line", | ||
| 1056 | }), | ||
| 1057 | ) | ||
| 1058 | .await; | ||
| 1059 | |||
| 1060 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 1061 | let workspace = init_test_workspace(&project, cx).await; | ||
| 1062 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 1063 | |||
| 1064 | let task = project.update(cx, |project, cx| { | ||
| 1065 | project.start_debug_session( | ||
| 1066 | dap::test_config(DebugRequestType::Launch, Some(true), None), | ||
| 1067 | cx, | ||
| 1068 | ) | ||
| 1069 | }); | ||
| 1070 | |||
| 1071 | assert!( | ||
| 1072 | task.await.is_err(), | ||
| 1073 | "Session should failed to start if launch request fails" | ||
| 1074 | ); | ||
| 1075 | |||
| 1076 | cx.run_until_parked(); | ||
| 1077 | |||
| 1078 | project.update(cx, |project, cx| { | ||
| 1079 | assert!( | ||
| 1080 | project.dap_store().read(cx).sessions().count() == 0, | ||
| 1081 | "Session wouldn't exist if it was shutdown" | ||
| 1082 | ); | ||
| 1083 | }); | ||
| 1084 | } | ||
| @@ -0,0 +1,262 @@ | |||
| 1 | use crate::{ | ||
| 2 | debugger_panel::DebugPanel, | ||
| 3 | session::ThreadItem, | ||
| 4 | tests::{active_debug_session_panel, init_test, init_test_workspace}, | ||
| 5 | }; | ||
| 6 | use dap::{ | ||
| 7 | requests::{Modules, StackTrace, Threads}, | ||
| 8 | DebugRequestType, StoppedEvent, | ||
| 9 | }; | ||
| 10 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 11 | use project::{FakeFs, Project}; | ||
| 12 | use std::sync::{ | ||
| 13 | atomic::{AtomicBool, AtomicI32, Ordering}, | ||
| 14 | Arc, | ||
| 15 | }; | ||
| 16 | |||
| 17 | #[gpui::test] | ||
| 18 | async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 19 | init_test(cx); | ||
| 20 | |||
| 21 | let fs = FakeFs::new(executor.clone()); | ||
| 22 | |||
| 23 | let project = Project::test(fs, ["/project".as_ref()], cx).await; | ||
| 24 | let workspace = init_test_workspace(&project, cx).await; | ||
| 25 | workspace | ||
| 26 | .update(cx, |workspace, window, cx| { | ||
| 27 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 28 | }) | ||
| 29 | .unwrap(); | ||
| 30 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 31 | |||
| 32 | let task = project.update(cx, |project, cx| { | ||
| 33 | project.start_debug_session( | ||
| 34 | dap::test_config( | ||
| 35 | DebugRequestType::Launch, | ||
| 36 | None, | ||
| 37 | Some(dap::Capabilities { | ||
| 38 | supports_modules_request: Some(true), | ||
| 39 | ..Default::default() | ||
| 40 | }), | ||
| 41 | ), | ||
| 42 | cx, | ||
| 43 | ) | ||
| 44 | }); | ||
| 45 | |||
| 46 | let session = task.await.unwrap(); | ||
| 47 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 48 | |||
| 49 | client | ||
| 50 | .on_request::<StackTrace, _>(move |_, args| { | ||
| 51 | assert!(args.thread_id == 1); | ||
| 52 | Ok(dap::StackTraceResponse { | ||
| 53 | stack_frames: Vec::default(), | ||
| 54 | total_frames: None, | ||
| 55 | }) | ||
| 56 | }) | ||
| 57 | .await; | ||
| 58 | |||
| 59 | let called_modules = Arc::new(AtomicBool::new(false)); | ||
| 60 | let modules = vec![ | ||
| 61 | dap::Module { | ||
| 62 | id: dap::ModuleId::Number(1), | ||
| 63 | name: "First Module".into(), | ||
| 64 | address_range: None, | ||
| 65 | date_time_stamp: None, | ||
| 66 | path: None, | ||
| 67 | symbol_file_path: None, | ||
| 68 | symbol_status: None, | ||
| 69 | version: None, | ||
| 70 | is_optimized: None, | ||
| 71 | is_user_code: None, | ||
| 72 | }, | ||
| 73 | dap::Module { | ||
| 74 | id: dap::ModuleId::Number(2), | ||
| 75 | name: "Second Module".into(), | ||
| 76 | address_range: None, | ||
| 77 | date_time_stamp: None, | ||
| 78 | path: None, | ||
| 79 | symbol_file_path: None, | ||
| 80 | symbol_status: None, | ||
| 81 | version: None, | ||
| 82 | is_optimized: None, | ||
| 83 | is_user_code: None, | ||
| 84 | }, | ||
| 85 | ]; | ||
| 86 | |||
| 87 | client | ||
| 88 | .on_request::<Threads, _>(move |_, _| { | ||
| 89 | Ok(dap::ThreadsResponse { | ||
| 90 | threads: vec![dap::Thread { | ||
| 91 | id: 1, | ||
| 92 | name: "Thread 1".into(), | ||
| 93 | }], | ||
| 94 | }) | ||
| 95 | }) | ||
| 96 | .await; | ||
| 97 | |||
| 98 | client | ||
| 99 | .on_request::<Modules, _>({ | ||
| 100 | let called_modules = called_modules.clone(); | ||
| 101 | let modules_request_count = AtomicI32::new(0); | ||
| 102 | let modules = modules.clone(); | ||
| 103 | move |_, _| { | ||
| 104 | modules_request_count.fetch_add(1, Ordering::SeqCst); | ||
| 105 | assert_eq!( | ||
| 106 | 1, | ||
| 107 | modules_request_count.load(Ordering::SeqCst), | ||
| 108 | "This request should only be called once from the host" | ||
| 109 | ); | ||
| 110 | called_modules.store(true, Ordering::SeqCst); | ||
| 111 | |||
| 112 | Ok(dap::ModulesResponse { | ||
| 113 | modules: modules.clone(), | ||
| 114 | total_modules: Some(2u64), | ||
| 115 | }) | ||
| 116 | } | ||
| 117 | }) | ||
| 118 | .await; | ||
| 119 | |||
| 120 | client | ||
| 121 | .fake_event(dap::messages::Events::Stopped(StoppedEvent { | ||
| 122 | reason: dap::StoppedEventReason::Pause, | ||
| 123 | description: None, | ||
| 124 | thread_id: Some(1), | ||
| 125 | preserve_focus_hint: None, | ||
| 126 | text: None, | ||
| 127 | all_threads_stopped: None, | ||
| 128 | hit_breakpoint_ids: None, | ||
| 129 | })) | ||
| 130 | .await; | ||
| 131 | |||
| 132 | cx.run_until_parked(); | ||
| 133 | |||
| 134 | let running_state = | ||
| 135 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 136 | cx.focus_self(window); | ||
| 137 | item.mode() | ||
| 138 | .as_running() | ||
| 139 | .expect("Session should be running by this point") | ||
| 140 | .clone() | ||
| 141 | }); | ||
| 142 | |||
| 143 | assert!( | ||
| 144 | !called_modules.load(std::sync::atomic::Ordering::SeqCst), | ||
| 145 | "Request Modules shouldn't be called before it's needed" | ||
| 146 | ); | ||
| 147 | |||
| 148 | running_state.update(cx, |state, cx| { | ||
| 149 | state.set_thread_item(ThreadItem::Modules, cx); | ||
| 150 | cx.refresh_windows(); | ||
| 151 | }); | ||
| 152 | |||
| 153 | cx.run_until_parked(); | ||
| 154 | |||
| 155 | assert!( | ||
| 156 | called_modules.load(std::sync::atomic::Ordering::SeqCst), | ||
| 157 | "Request Modules should be called because a user clicked on the module list" | ||
| 158 | ); | ||
| 159 | |||
| 160 | active_debug_session_panel(workspace, cx).update(cx, |_, cx| { | ||
| 161 | running_state.update(cx, |state, cx| { | ||
| 162 | state.set_thread_item(ThreadItem::Modules, cx) | ||
| 163 | }); | ||
| 164 | let actual_modules = running_state.update(cx, |state, cx| { | ||
| 165 | state.module_list().update(cx, |list, cx| list.modules(cx)) | ||
| 166 | }); | ||
| 167 | |||
| 168 | assert_eq!(modules, actual_modules); | ||
| 169 | }); | ||
| 170 | |||
| 171 | // Test all module events now | ||
| 172 | // New Module | ||
| 173 | // Changed | ||
| 174 | // Removed | ||
| 175 | |||
| 176 | let new_module = dap::Module { | ||
| 177 | id: dap::ModuleId::Number(3), | ||
| 178 | name: "Third Module".into(), | ||
| 179 | address_range: None, | ||
| 180 | date_time_stamp: None, | ||
| 181 | path: None, | ||
| 182 | symbol_file_path: None, | ||
| 183 | symbol_status: None, | ||
| 184 | version: None, | ||
| 185 | is_optimized: None, | ||
| 186 | is_user_code: None, | ||
| 187 | }; | ||
| 188 | |||
| 189 | client | ||
| 190 | .fake_event(dap::messages::Events::Module(dap::ModuleEvent { | ||
| 191 | reason: dap::ModuleEventReason::New, | ||
| 192 | module: new_module.clone(), | ||
| 193 | })) | ||
| 194 | .await; | ||
| 195 | |||
| 196 | cx.run_until_parked(); | ||
| 197 | |||
| 198 | active_debug_session_panel(workspace, cx).update(cx, |_, cx| { | ||
| 199 | let actual_modules = running_state.update(cx, |state, cx| { | ||
| 200 | state.module_list().update(cx, |list, cx| list.modules(cx)) | ||
| 201 | }); | ||
| 202 | assert_eq!(actual_modules.len(), 3); | ||
| 203 | assert!(actual_modules.contains(&new_module)); | ||
| 204 | }); | ||
| 205 | |||
| 206 | let changed_module = dap::Module { | ||
| 207 | id: dap::ModuleId::Number(2), | ||
| 208 | name: "Modified Second Module".into(), | ||
| 209 | address_range: None, | ||
| 210 | date_time_stamp: None, | ||
| 211 | path: None, | ||
| 212 | symbol_file_path: None, | ||
| 213 | symbol_status: None, | ||
| 214 | version: None, | ||
| 215 | is_optimized: None, | ||
| 216 | is_user_code: None, | ||
| 217 | }; | ||
| 218 | |||
| 219 | client | ||
| 220 | .fake_event(dap::messages::Events::Module(dap::ModuleEvent { | ||
| 221 | reason: dap::ModuleEventReason::Changed, | ||
| 222 | module: changed_module.clone(), | ||
| 223 | })) | ||
| 224 | .await; | ||
| 225 | |||
| 226 | cx.run_until_parked(); | ||
| 227 | |||
| 228 | active_debug_session_panel(workspace, cx).update(cx, |_, cx| { | ||
| 229 | let actual_modules = running_state.update(cx, |state, cx| { | ||
| 230 | state.module_list().update(cx, |list, cx| list.modules(cx)) | ||
| 231 | }); | ||
| 232 | |||
| 233 | assert_eq!(actual_modules.len(), 3); | ||
| 234 | assert!(actual_modules.contains(&changed_module)); | ||
| 235 | }); | ||
| 236 | |||
| 237 | client | ||
| 238 | .fake_event(dap::messages::Events::Module(dap::ModuleEvent { | ||
| 239 | reason: dap::ModuleEventReason::Removed, | ||
| 240 | module: changed_module.clone(), | ||
| 241 | })) | ||
| 242 | .await; | ||
| 243 | |||
| 244 | cx.run_until_parked(); | ||
| 245 | |||
| 246 | active_debug_session_panel(workspace, cx).update(cx, |_, cx| { | ||
| 247 | let actual_modules = running_state.update(cx, |state, cx| { | ||
| 248 | state.module_list().update(cx, |list, cx| list.modules(cx)) | ||
| 249 | }); | ||
| 250 | |||
| 251 | assert_eq!(actual_modules.len(), 2); | ||
| 252 | assert!(!actual_modules.contains(&changed_module)); | ||
| 253 | }); | ||
| 254 | |||
| 255 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 256 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 257 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 258 | }) | ||
| 259 | }); | ||
| 260 | |||
| 261 | shutdown_session.await.unwrap(); | ||
| 262 | } | ||
| @@ -0,0 +1,845 @@ | |||
| 1 | use crate::{ | ||
| 2 | debugger_panel::DebugPanel, | ||
| 3 | session::running::stack_frame_list::StackFrameEntry, | ||
| 4 | tests::{active_debug_session_panel, init_test, init_test_workspace}, | ||
| 5 | }; | ||
| 6 | use dap::{ | ||
| 7 | requests::{StackTrace, Threads}, | ||
| 8 | StackFrame, | ||
| 9 | }; | ||
| 10 | use editor::{Editor, ToPoint as _}; | ||
| 11 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 12 | use project::{FakeFs, Project}; | ||
| 13 | use serde_json::json; | ||
| 14 | use std::sync::Arc; | ||
| 15 | use unindent::Unindent as _; | ||
| 16 | use util::path; | ||
| 17 | |||
| 18 | #[gpui::test] | ||
| 19 | async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( | ||
| 20 | executor: BackgroundExecutor, | ||
| 21 | cx: &mut TestAppContext, | ||
| 22 | ) { | ||
| 23 | init_test(cx); | ||
| 24 | |||
| 25 | let fs = FakeFs::new(executor.clone()); | ||
| 26 | |||
| 27 | let test_file_content = r#" | ||
| 28 | import { SOME_VALUE } './module.js'; | ||
| 29 | |||
| 30 | console.log(SOME_VALUE); | ||
| 31 | "# | ||
| 32 | .unindent(); | ||
| 33 | |||
| 34 | let module_file_content = r#" | ||
| 35 | export SOME_VALUE = 'some value'; | ||
| 36 | "# | ||
| 37 | .unindent(); | ||
| 38 | |||
| 39 | fs.insert_tree( | ||
| 40 | path!("/project"), | ||
| 41 | json!({ | ||
| 42 | "src": { | ||
| 43 | "test.js": test_file_content, | ||
| 44 | "module.js": module_file_content, | ||
| 45 | } | ||
| 46 | }), | ||
| 47 | ) | ||
| 48 | .await; | ||
| 49 | |||
| 50 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 51 | let workspace = init_test_workspace(&project, cx).await; | ||
| 52 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 53 | |||
| 54 | let task = project.update(cx, |project, cx| { | ||
| 55 | project.start_debug_session( | ||
| 56 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 57 | cx, | ||
| 58 | ) | ||
| 59 | }); | ||
| 60 | |||
| 61 | let session = task.await.unwrap(); | ||
| 62 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 63 | |||
| 64 | client | ||
| 65 | .on_request::<Threads, _>(move |_, _| { | ||
| 66 | Ok(dap::ThreadsResponse { | ||
| 67 | threads: vec![dap::Thread { | ||
| 68 | id: 1, | ||
| 69 | name: "Thread 1".into(), | ||
| 70 | }], | ||
| 71 | }) | ||
| 72 | }) | ||
| 73 | .await; | ||
| 74 | |||
| 75 | let stack_frames = vec![ | ||
| 76 | StackFrame { | ||
| 77 | id: 1, | ||
| 78 | name: "Stack Frame 1".into(), | ||
| 79 | source: Some(dap::Source { | ||
| 80 | name: Some("test.js".into()), | ||
| 81 | path: Some(path!("/project/src/test.js").into()), | ||
| 82 | source_reference: None, | ||
| 83 | presentation_hint: None, | ||
| 84 | origin: None, | ||
| 85 | sources: None, | ||
| 86 | adapter_data: None, | ||
| 87 | checksums: None, | ||
| 88 | }), | ||
| 89 | line: 3, | ||
| 90 | column: 1, | ||
| 91 | end_line: None, | ||
| 92 | end_column: None, | ||
| 93 | can_restart: None, | ||
| 94 | instruction_pointer_reference: None, | ||
| 95 | module_id: None, | ||
| 96 | presentation_hint: None, | ||
| 97 | }, | ||
| 98 | StackFrame { | ||
| 99 | id: 2, | ||
| 100 | name: "Stack Frame 2".into(), | ||
| 101 | source: Some(dap::Source { | ||
| 102 | name: Some("module.js".into()), | ||
| 103 | path: Some(path!("/project/src/module.js").into()), | ||
| 104 | source_reference: None, | ||
| 105 | presentation_hint: None, | ||
| 106 | origin: None, | ||
| 107 | sources: None, | ||
| 108 | adapter_data: None, | ||
| 109 | checksums: None, | ||
| 110 | }), | ||
| 111 | line: 1, | ||
| 112 | column: 1, | ||
| 113 | end_line: None, | ||
| 114 | end_column: None, | ||
| 115 | can_restart: None, | ||
| 116 | instruction_pointer_reference: None, | ||
| 117 | module_id: None, | ||
| 118 | presentation_hint: None, | ||
| 119 | }, | ||
| 120 | ]; | ||
| 121 | |||
| 122 | client | ||
| 123 | .on_request::<StackTrace, _>({ | ||
| 124 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 125 | move |_, args| { | ||
| 126 | assert_eq!(1, args.thread_id); | ||
| 127 | |||
| 128 | Ok(dap::StackTraceResponse { | ||
| 129 | stack_frames: (*stack_frames).clone(), | ||
| 130 | total_frames: None, | ||
| 131 | }) | ||
| 132 | } | ||
| 133 | }) | ||
| 134 | .await; | ||
| 135 | |||
| 136 | client | ||
| 137 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 138 | reason: dap::StoppedEventReason::Pause, | ||
| 139 | description: None, | ||
| 140 | thread_id: Some(1), | ||
| 141 | preserve_focus_hint: None, | ||
| 142 | text: None, | ||
| 143 | all_threads_stopped: None, | ||
| 144 | hit_breakpoint_ids: None, | ||
| 145 | })) | ||
| 146 | .await; | ||
| 147 | |||
| 148 | cx.run_until_parked(); | ||
| 149 | |||
| 150 | // trigger to load threads | ||
| 151 | active_debug_session_panel(workspace, cx).update(cx, |session, cx| { | ||
| 152 | session | ||
| 153 | .mode() | ||
| 154 | .as_running() | ||
| 155 | .unwrap() | ||
| 156 | .update(cx, |running_state, cx| { | ||
| 157 | running_state | ||
| 158 | .session() | ||
| 159 | .update(cx, |session, cx| session.threads(cx)); | ||
| 160 | }); | ||
| 161 | }); | ||
| 162 | |||
| 163 | cx.run_until_parked(); | ||
| 164 | |||
| 165 | // select first thread | ||
| 166 | active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| { | ||
| 167 | session | ||
| 168 | .mode() | ||
| 169 | .as_running() | ||
| 170 | .unwrap() | ||
| 171 | .update(cx, |running_state, cx| { | ||
| 172 | running_state.select_current_thread( | ||
| 173 | &running_state | ||
| 174 | .session() | ||
| 175 | .update(cx, |session, cx| session.threads(cx)), | ||
| 176 | cx, | ||
| 177 | ); | ||
| 178 | }); | ||
| 179 | }); | ||
| 180 | |||
| 181 | cx.run_until_parked(); | ||
| 182 | |||
| 183 | active_debug_session_panel(workspace, cx).update(cx, |session, cx| { | ||
| 184 | let stack_frame_list = session | ||
| 185 | .mode() | ||
| 186 | .as_running() | ||
| 187 | .unwrap() | ||
| 188 | .update(cx, |state, _| state.stack_frame_list().clone()); | ||
| 189 | |||
| 190 | stack_frame_list.update(cx, |stack_frame_list, cx| { | ||
| 191 | assert_eq!(Some(1), stack_frame_list.current_stack_frame_id()); | ||
| 192 | assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx)); | ||
| 193 | }); | ||
| 194 | }); | ||
| 195 | |||
| 196 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 197 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 198 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 199 | }) | ||
| 200 | }); | ||
| 201 | |||
| 202 | shutdown_session.await.unwrap(); | ||
| 203 | } | ||
| 204 | |||
| 205 | #[gpui::test] | ||
| 206 | async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 207 | init_test(cx); | ||
| 208 | |||
| 209 | let fs = FakeFs::new(executor.clone()); | ||
| 210 | |||
| 211 | let test_file_content = r#" | ||
| 212 | import { SOME_VALUE } './module.js'; | ||
| 213 | |||
| 214 | console.log(SOME_VALUE); | ||
| 215 | "# | ||
| 216 | .unindent(); | ||
| 217 | |||
| 218 | let module_file_content = r#" | ||
| 219 | export SOME_VALUE = 'some value'; | ||
| 220 | "# | ||
| 221 | .unindent(); | ||
| 222 | |||
| 223 | fs.insert_tree( | ||
| 224 | path!("/project"), | ||
| 225 | json!({ | ||
| 226 | "src": { | ||
| 227 | "test.js": test_file_content, | ||
| 228 | "module.js": module_file_content, | ||
| 229 | } | ||
| 230 | }), | ||
| 231 | ) | ||
| 232 | .await; | ||
| 233 | |||
| 234 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 235 | let workspace = init_test_workspace(&project, cx).await; | ||
| 236 | let _ = workspace.update(cx, |workspace, window, cx| { | ||
| 237 | workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx); | ||
| 238 | }); | ||
| 239 | |||
| 240 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 241 | |||
| 242 | let task = project.update(cx, |project, cx| { | ||
| 243 | project.start_debug_session( | ||
| 244 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 245 | cx, | ||
| 246 | ) | ||
| 247 | }); | ||
| 248 | |||
| 249 | let session = task.await.unwrap(); | ||
| 250 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 251 | |||
| 252 | client | ||
| 253 | .on_request::<Threads, _>(move |_, _| { | ||
| 254 | Ok(dap::ThreadsResponse { | ||
| 255 | threads: vec![dap::Thread { | ||
| 256 | id: 1, | ||
| 257 | name: "Thread 1".into(), | ||
| 258 | }], | ||
| 259 | }) | ||
| 260 | }) | ||
| 261 | .await; | ||
| 262 | |||
| 263 | let stack_frames = vec![ | ||
| 264 | StackFrame { | ||
| 265 | id: 1, | ||
| 266 | name: "Stack Frame 1".into(), | ||
| 267 | source: Some(dap::Source { | ||
| 268 | name: Some("test.js".into()), | ||
| 269 | path: Some(path!("/project/src/test.js").into()), | ||
| 270 | source_reference: None, | ||
| 271 | presentation_hint: None, | ||
| 272 | origin: None, | ||
| 273 | sources: None, | ||
| 274 | adapter_data: None, | ||
| 275 | checksums: None, | ||
| 276 | }), | ||
| 277 | line: 3, | ||
| 278 | column: 1, | ||
| 279 | end_line: None, | ||
| 280 | end_column: None, | ||
| 281 | can_restart: None, | ||
| 282 | instruction_pointer_reference: None, | ||
| 283 | module_id: None, | ||
| 284 | presentation_hint: None, | ||
| 285 | }, | ||
| 286 | StackFrame { | ||
| 287 | id: 2, | ||
| 288 | name: "Stack Frame 2".into(), | ||
| 289 | source: Some(dap::Source { | ||
| 290 | name: Some("module.js".into()), | ||
| 291 | path: Some(path!("/project/src/module.js").into()), | ||
| 292 | source_reference: None, | ||
| 293 | presentation_hint: None, | ||
| 294 | origin: None, | ||
| 295 | sources: None, | ||
| 296 | adapter_data: None, | ||
| 297 | checksums: None, | ||
| 298 | }), | ||
| 299 | line: 1, | ||
| 300 | column: 1, | ||
| 301 | end_line: None, | ||
| 302 | end_column: None, | ||
| 303 | can_restart: None, | ||
| 304 | instruction_pointer_reference: None, | ||
| 305 | module_id: None, | ||
| 306 | presentation_hint: None, | ||
| 307 | }, | ||
| 308 | ]; | ||
| 309 | |||
| 310 | client | ||
| 311 | .on_request::<StackTrace, _>({ | ||
| 312 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 313 | move |_, args| { | ||
| 314 | assert_eq!(1, args.thread_id); | ||
| 315 | |||
| 316 | Ok(dap::StackTraceResponse { | ||
| 317 | stack_frames: (*stack_frames).clone(), | ||
| 318 | total_frames: None, | ||
| 319 | }) | ||
| 320 | } | ||
| 321 | }) | ||
| 322 | .await; | ||
| 323 | |||
| 324 | client | ||
| 325 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 326 | reason: dap::StoppedEventReason::Pause, | ||
| 327 | description: None, | ||
| 328 | thread_id: Some(1), | ||
| 329 | preserve_focus_hint: None, | ||
| 330 | text: None, | ||
| 331 | all_threads_stopped: None, | ||
| 332 | hit_breakpoint_ids: None, | ||
| 333 | })) | ||
| 334 | .await; | ||
| 335 | |||
| 336 | cx.run_until_parked(); | ||
| 337 | |||
| 338 | // trigger threads to load | ||
| 339 | active_debug_session_panel(workspace, cx).update(cx, |session, cx| { | ||
| 340 | session | ||
| 341 | .mode() | ||
| 342 | .as_running() | ||
| 343 | .unwrap() | ||
| 344 | .update(cx, |running_state, cx| { | ||
| 345 | running_state | ||
| 346 | .session() | ||
| 347 | .update(cx, |session, cx| session.threads(cx)); | ||
| 348 | }); | ||
| 349 | }); | ||
| 350 | |||
| 351 | cx.run_until_parked(); | ||
| 352 | |||
| 353 | // select first thread | ||
| 354 | active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| { | ||
| 355 | session | ||
| 356 | .mode() | ||
| 357 | .as_running() | ||
| 358 | .unwrap() | ||
| 359 | .update(cx, |running_state, cx| { | ||
| 360 | running_state.select_current_thread( | ||
| 361 | &running_state | ||
| 362 | .session() | ||
| 363 | .update(cx, |session, cx| session.threads(cx)), | ||
| 364 | cx, | ||
| 365 | ); | ||
| 366 | }); | ||
| 367 | }); | ||
| 368 | |||
| 369 | cx.run_until_parked(); | ||
| 370 | |||
| 371 | workspace | ||
| 372 | .update(cx, |workspace, window, cx| { | ||
| 373 | let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>(); | ||
| 374 | assert_eq!(1, editors.len()); | ||
| 375 | |||
| 376 | let project_path = editors[0] | ||
| 377 | .update(cx, |editor, cx| editor.project_path(cx)) | ||
| 378 | .unwrap(); | ||
| 379 | let expected = if cfg!(target_os = "windows") { | ||
| 380 | "src\\test.js" | ||
| 381 | } else { | ||
| 382 | "src/test.js" | ||
| 383 | }; | ||
| 384 | assert_eq!(expected, project_path.path.to_string_lossy()); | ||
| 385 | assert_eq!(test_file_content, editors[0].read(cx).text(cx)); | ||
| 386 | assert_eq!( | ||
| 387 | vec![2..3], | ||
| 388 | editors[0].update(cx, |editor, cx| { | ||
| 389 | let snapshot = editor.snapshot(window, cx); | ||
| 390 | |||
| 391 | editor | ||
| 392 | .highlighted_rows::<editor::DebugCurrentRowHighlight>() | ||
| 393 | .map(|(range, _)| { | ||
| 394 | let start = range.start.to_point(&snapshot.buffer_snapshot); | ||
| 395 | let end = range.end.to_point(&snapshot.buffer_snapshot); | ||
| 396 | start.row..end.row | ||
| 397 | }) | ||
| 398 | .collect::<Vec<_>>() | ||
| 399 | }) | ||
| 400 | ); | ||
| 401 | }) | ||
| 402 | .unwrap(); | ||
| 403 | |||
| 404 | let stack_frame_list = workspace | ||
| 405 | .update(cx, |workspace, _window, cx| { | ||
| 406 | let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); | ||
| 407 | let active_debug_panel_item = debug_panel | ||
| 408 | .update(cx, |this, cx| this.active_session(cx)) | ||
| 409 | .unwrap(); | ||
| 410 | |||
| 411 | active_debug_panel_item | ||
| 412 | .read(cx) | ||
| 413 | .mode() | ||
| 414 | .as_running() | ||
| 415 | .unwrap() | ||
| 416 | .read(cx) | ||
| 417 | .stack_frame_list() | ||
| 418 | .clone() | ||
| 419 | }) | ||
| 420 | .unwrap(); | ||
| 421 | |||
| 422 | stack_frame_list.update(cx, |stack_frame_list, cx| { | ||
| 423 | assert_eq!(Some(1), stack_frame_list.current_stack_frame_id()); | ||
| 424 | assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx)); | ||
| 425 | }); | ||
| 426 | |||
| 427 | // select second stack frame | ||
| 428 | stack_frame_list | ||
| 429 | .update_in(cx, |stack_frame_list, window, cx| { | ||
| 430 | stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) | ||
| 431 | }) | ||
| 432 | .await | ||
| 433 | .unwrap(); | ||
| 434 | |||
| 435 | cx.run_until_parked(); | ||
| 436 | |||
| 437 | stack_frame_list.update(cx, |stack_frame_list, cx| { | ||
| 438 | assert_eq!(Some(2), stack_frame_list.current_stack_frame_id()); | ||
| 439 | assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx)); | ||
| 440 | }); | ||
| 441 | |||
| 442 | let _ = workspace.update(cx, |workspace, window, cx| { | ||
| 443 | let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>(); | ||
| 444 | assert_eq!(1, editors.len()); | ||
| 445 | |||
| 446 | let project_path = editors[0] | ||
| 447 | .update(cx, |editor, cx| editor.project_path(cx)) | ||
| 448 | .unwrap(); | ||
| 449 | let expected = if cfg!(target_os = "windows") { | ||
| 450 | "src\\module.js" | ||
| 451 | } else { | ||
| 452 | "src/module.js" | ||
| 453 | }; | ||
| 454 | assert_eq!(expected, project_path.path.to_string_lossy()); | ||
| 455 | assert_eq!(module_file_content, editors[0].read(cx).text(cx)); | ||
| 456 | assert_eq!( | ||
| 457 | vec![0..1], | ||
| 458 | editors[0].update(cx, |editor, cx| { | ||
| 459 | let snapshot = editor.snapshot(window, cx); | ||
| 460 | |||
| 461 | editor | ||
| 462 | .highlighted_rows::<editor::DebugCurrentRowHighlight>() | ||
| 463 | .map(|(range, _)| { | ||
| 464 | let start = range.start.to_point(&snapshot.buffer_snapshot); | ||
| 465 | let end = range.end.to_point(&snapshot.buffer_snapshot); | ||
| 466 | start.row..end.row | ||
| 467 | }) | ||
| 468 | .collect::<Vec<_>>() | ||
| 469 | }) | ||
| 470 | ); | ||
| 471 | }); | ||
| 472 | |||
| 473 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 474 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 475 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 476 | }) | ||
| 477 | }); | ||
| 478 | |||
| 479 | shutdown_session.await.unwrap(); | ||
| 480 | } | ||
| 481 | |||
| 482 | #[gpui::test] | ||
| 483 | async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 484 | init_test(cx); | ||
| 485 | |||
| 486 | let fs = FakeFs::new(executor.clone()); | ||
| 487 | |||
| 488 | let test_file_content = r#" | ||
| 489 | import { SOME_VALUE } './module.js'; | ||
| 490 | |||
| 491 | console.log(SOME_VALUE); | ||
| 492 | "# | ||
| 493 | .unindent(); | ||
| 494 | |||
| 495 | let module_file_content = r#" | ||
| 496 | export SOME_VALUE = 'some value'; | ||
| 497 | "# | ||
| 498 | .unindent(); | ||
| 499 | |||
| 500 | fs.insert_tree( | ||
| 501 | path!("/project"), | ||
| 502 | json!({ | ||
| 503 | "src": { | ||
| 504 | "test.js": test_file_content, | ||
| 505 | "module.js": module_file_content, | ||
| 506 | } | ||
| 507 | }), | ||
| 508 | ) | ||
| 509 | .await; | ||
| 510 | |||
| 511 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 512 | let workspace = init_test_workspace(&project, cx).await; | ||
| 513 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 514 | |||
| 515 | let task = project.update(cx, |project, cx| { | ||
| 516 | project.start_debug_session( | ||
| 517 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 518 | cx, | ||
| 519 | ) | ||
| 520 | }); | ||
| 521 | |||
| 522 | let session = task.await.unwrap(); | ||
| 523 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 524 | |||
| 525 | client | ||
| 526 | .on_request::<Threads, _>(move |_, _| { | ||
| 527 | Ok(dap::ThreadsResponse { | ||
| 528 | threads: vec![dap::Thread { | ||
| 529 | id: 1, | ||
| 530 | name: "Thread 1".into(), | ||
| 531 | }], | ||
| 532 | }) | ||
| 533 | }) | ||
| 534 | .await; | ||
| 535 | |||
| 536 | let stack_frames = vec![ | ||
| 537 | StackFrame { | ||
| 538 | id: 1, | ||
| 539 | name: "Stack Frame 1".into(), | ||
| 540 | source: Some(dap::Source { | ||
| 541 | name: Some("test.js".into()), | ||
| 542 | path: Some(path!("/project/src/test.js").into()), | ||
| 543 | source_reference: None, | ||
| 544 | presentation_hint: None, | ||
| 545 | origin: None, | ||
| 546 | sources: None, | ||
| 547 | adapter_data: None, | ||
| 548 | checksums: None, | ||
| 549 | }), | ||
| 550 | line: 3, | ||
| 551 | column: 1, | ||
| 552 | end_line: None, | ||
| 553 | end_column: None, | ||
| 554 | can_restart: None, | ||
| 555 | instruction_pointer_reference: None, | ||
| 556 | module_id: None, | ||
| 557 | presentation_hint: None, | ||
| 558 | }, | ||
| 559 | StackFrame { | ||
| 560 | id: 2, | ||
| 561 | name: "Stack Frame 2".into(), | ||
| 562 | source: Some(dap::Source { | ||
| 563 | name: Some("module.js".into()), | ||
| 564 | path: Some(path!("/project/src/module.js").into()), | ||
| 565 | source_reference: None, | ||
| 566 | presentation_hint: None, | ||
| 567 | origin: Some("ignored".into()), | ||
| 568 | sources: None, | ||
| 569 | adapter_data: None, | ||
| 570 | checksums: None, | ||
| 571 | }), | ||
| 572 | line: 1, | ||
| 573 | column: 1, | ||
| 574 | end_line: None, | ||
| 575 | end_column: None, | ||
| 576 | can_restart: None, | ||
| 577 | instruction_pointer_reference: None, | ||
| 578 | module_id: None, | ||
| 579 | presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), | ||
| 580 | }, | ||
| 581 | StackFrame { | ||
| 582 | id: 3, | ||
| 583 | name: "Stack Frame 3".into(), | ||
| 584 | source: Some(dap::Source { | ||
| 585 | name: Some("module.js".into()), | ||
| 586 | path: Some(path!("/project/src/module.js").into()), | ||
| 587 | source_reference: None, | ||
| 588 | presentation_hint: None, | ||
| 589 | origin: Some("ignored".into()), | ||
| 590 | sources: None, | ||
| 591 | adapter_data: None, | ||
| 592 | checksums: None, | ||
| 593 | }), | ||
| 594 | line: 1, | ||
| 595 | column: 1, | ||
| 596 | end_line: None, | ||
| 597 | end_column: None, | ||
| 598 | can_restart: None, | ||
| 599 | instruction_pointer_reference: None, | ||
| 600 | module_id: None, | ||
| 601 | presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), | ||
| 602 | }, | ||
| 603 | StackFrame { | ||
| 604 | id: 4, | ||
| 605 | name: "Stack Frame 4".into(), | ||
| 606 | source: Some(dap::Source { | ||
| 607 | name: Some("module.js".into()), | ||
| 608 | path: Some(path!("/project/src/module.js").into()), | ||
| 609 | source_reference: None, | ||
| 610 | presentation_hint: None, | ||
| 611 | origin: None, | ||
| 612 | sources: None, | ||
| 613 | adapter_data: None, | ||
| 614 | checksums: None, | ||
| 615 | }), | ||
| 616 | line: 1, | ||
| 617 | column: 1, | ||
| 618 | end_line: None, | ||
| 619 | end_column: None, | ||
| 620 | can_restart: None, | ||
| 621 | instruction_pointer_reference: None, | ||
| 622 | module_id: None, | ||
| 623 | presentation_hint: None, | ||
| 624 | }, | ||
| 625 | StackFrame { | ||
| 626 | id: 5, | ||
| 627 | name: "Stack Frame 5".into(), | ||
| 628 | source: Some(dap::Source { | ||
| 629 | name: Some("module.js".into()), | ||
| 630 | path: Some(path!("/project/src/module.js").into()), | ||
| 631 | source_reference: None, | ||
| 632 | presentation_hint: None, | ||
| 633 | origin: None, | ||
| 634 | sources: None, | ||
| 635 | adapter_data: None, | ||
| 636 | checksums: None, | ||
| 637 | }), | ||
| 638 | line: 1, | ||
| 639 | column: 1, | ||
| 640 | end_line: None, | ||
| 641 | end_column: None, | ||
| 642 | can_restart: None, | ||
| 643 | instruction_pointer_reference: None, | ||
| 644 | module_id: None, | ||
| 645 | presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), | ||
| 646 | }, | ||
| 647 | StackFrame { | ||
| 648 | id: 6, | ||
| 649 | name: "Stack Frame 6".into(), | ||
| 650 | source: Some(dap::Source { | ||
| 651 | name: Some("module.js".into()), | ||
| 652 | path: Some(path!("/project/src/module.js").into()), | ||
| 653 | source_reference: None, | ||
| 654 | presentation_hint: None, | ||
| 655 | origin: None, | ||
| 656 | sources: None, | ||
| 657 | adapter_data: None, | ||
| 658 | checksums: None, | ||
| 659 | }), | ||
| 660 | line: 1, | ||
| 661 | column: 1, | ||
| 662 | end_line: None, | ||
| 663 | end_column: None, | ||
| 664 | can_restart: None, | ||
| 665 | instruction_pointer_reference: None, | ||
| 666 | module_id: None, | ||
| 667 | presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), | ||
| 668 | }, | ||
| 669 | StackFrame { | ||
| 670 | id: 7, | ||
| 671 | name: "Stack Frame 7".into(), | ||
| 672 | source: Some(dap::Source { | ||
| 673 | name: Some("module.js".into()), | ||
| 674 | path: Some(path!("/project/src/module.js").into()), | ||
| 675 | source_reference: None, | ||
| 676 | presentation_hint: None, | ||
| 677 | origin: None, | ||
| 678 | sources: None, | ||
| 679 | adapter_data: None, | ||
| 680 | checksums: None, | ||
| 681 | }), | ||
| 682 | line: 1, | ||
| 683 | column: 1, | ||
| 684 | end_line: None, | ||
| 685 | end_column: None, | ||
| 686 | can_restart: None, | ||
| 687 | instruction_pointer_reference: None, | ||
| 688 | module_id: None, | ||
| 689 | presentation_hint: None, | ||
| 690 | }, | ||
| 691 | ]; | ||
| 692 | |||
| 693 | client | ||
| 694 | .on_request::<StackTrace, _>({ | ||
| 695 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 696 | move |_, args| { | ||
| 697 | assert_eq!(1, args.thread_id); | ||
| 698 | |||
| 699 | Ok(dap::StackTraceResponse { | ||
| 700 | stack_frames: (*stack_frames).clone(), | ||
| 701 | total_frames: None, | ||
| 702 | }) | ||
| 703 | } | ||
| 704 | }) | ||
| 705 | .await; | ||
| 706 | |||
| 707 | client | ||
| 708 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 709 | reason: dap::StoppedEventReason::Pause, | ||
| 710 | description: None, | ||
| 711 | thread_id: Some(1), | ||
| 712 | preserve_focus_hint: None, | ||
| 713 | text: None, | ||
| 714 | all_threads_stopped: None, | ||
| 715 | hit_breakpoint_ids: None, | ||
| 716 | })) | ||
| 717 | .await; | ||
| 718 | |||
| 719 | cx.run_until_parked(); | ||
| 720 | |||
| 721 | // trigger threads to load | ||
| 722 | active_debug_session_panel(workspace, cx).update(cx, |session, cx| { | ||
| 723 | session | ||
| 724 | .mode() | ||
| 725 | .as_running() | ||
| 726 | .unwrap() | ||
| 727 | .update(cx, |running_state, cx| { | ||
| 728 | running_state | ||
| 729 | .session() | ||
| 730 | .update(cx, |session, cx| session.threads(cx)); | ||
| 731 | }); | ||
| 732 | }); | ||
| 733 | |||
| 734 | cx.run_until_parked(); | ||
| 735 | |||
| 736 | // select first thread | ||
| 737 | active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| { | ||
| 738 | session | ||
| 739 | .mode() | ||
| 740 | .as_running() | ||
| 741 | .unwrap() | ||
| 742 | .update(cx, |running_state, cx| { | ||
| 743 | running_state.select_current_thread( | ||
| 744 | &running_state | ||
| 745 | .session() | ||
| 746 | .update(cx, |session, cx| session.threads(cx)), | ||
| 747 | cx, | ||
| 748 | ); | ||
| 749 | }); | ||
| 750 | }); | ||
| 751 | |||
| 752 | cx.run_until_parked(); | ||
| 753 | |||
| 754 | // trigger stack frames to loaded | ||
| 755 | active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| { | ||
| 756 | let stack_frame_list = debug_panel_item | ||
| 757 | .mode() | ||
| 758 | .as_running() | ||
| 759 | .unwrap() | ||
| 760 | .update(cx, |state, _| state.stack_frame_list().clone()); | ||
| 761 | |||
| 762 | stack_frame_list.update(cx, |stack_frame_list, cx| { | ||
| 763 | stack_frame_list.dap_stack_frames(cx); | ||
| 764 | }); | ||
| 765 | }); | ||
| 766 | |||
| 767 | cx.run_until_parked(); | ||
| 768 | |||
| 769 | active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { | ||
| 770 | let stack_frame_list = debug_panel_item | ||
| 771 | .mode() | ||
| 772 | .as_running() | ||
| 773 | .unwrap() | ||
| 774 | .update(cx, |state, _| state.stack_frame_list().clone()); | ||
| 775 | |||
| 776 | stack_frame_list.update(cx, |stack_frame_list, cx| { | ||
| 777 | stack_frame_list.build_entries(true, window, cx); | ||
| 778 | |||
| 779 | assert_eq!( | ||
| 780 | &vec![ | ||
| 781 | StackFrameEntry::Normal(stack_frames[0].clone()), | ||
| 782 | StackFrameEntry::Collapsed(vec![ | ||
| 783 | stack_frames[1].clone(), | ||
| 784 | stack_frames[2].clone() | ||
| 785 | ]), | ||
| 786 | StackFrameEntry::Normal(stack_frames[3].clone()), | ||
| 787 | StackFrameEntry::Collapsed(vec![ | ||
| 788 | stack_frames[4].clone(), | ||
| 789 | stack_frames[5].clone() | ||
| 790 | ]), | ||
| 791 | StackFrameEntry::Normal(stack_frames[6].clone()), | ||
| 792 | ], | ||
| 793 | stack_frame_list.entries() | ||
| 794 | ); | ||
| 795 | |||
| 796 | stack_frame_list.expand_collapsed_entry( | ||
| 797 | 1, | ||
| 798 | &vec![stack_frames[1].clone(), stack_frames[2].clone()], | ||
| 799 | cx, | ||
| 800 | ); | ||
| 801 | |||
| 802 | assert_eq!( | ||
| 803 | &vec![ | ||
| 804 | StackFrameEntry::Normal(stack_frames[0].clone()), | ||
| 805 | StackFrameEntry::Normal(stack_frames[1].clone()), | ||
| 806 | StackFrameEntry::Normal(stack_frames[2].clone()), | ||
| 807 | StackFrameEntry::Normal(stack_frames[3].clone()), | ||
| 808 | StackFrameEntry::Collapsed(vec![ | ||
| 809 | stack_frames[4].clone(), | ||
| 810 | stack_frames[5].clone() | ||
| 811 | ]), | ||
| 812 | StackFrameEntry::Normal(stack_frames[6].clone()), | ||
| 813 | ], | ||
| 814 | stack_frame_list.entries() | ||
| 815 | ); | ||
| 816 | |||
| 817 | stack_frame_list.expand_collapsed_entry( | ||
| 818 | 4, | ||
| 819 | &vec![stack_frames[4].clone(), stack_frames[5].clone()], | ||
| 820 | cx, | ||
| 821 | ); | ||
| 822 | |||
| 823 | assert_eq!( | ||
| 824 | &vec![ | ||
| 825 | StackFrameEntry::Normal(stack_frames[0].clone()), | ||
| 826 | StackFrameEntry::Normal(stack_frames[1].clone()), | ||
| 827 | StackFrameEntry::Normal(stack_frames[2].clone()), | ||
| 828 | StackFrameEntry::Normal(stack_frames[3].clone()), | ||
| 829 | StackFrameEntry::Normal(stack_frames[4].clone()), | ||
| 830 | StackFrameEntry::Normal(stack_frames[5].clone()), | ||
| 831 | StackFrameEntry::Normal(stack_frames[6].clone()), | ||
| 832 | ], | ||
| 833 | stack_frame_list.entries() | ||
| 834 | ); | ||
| 835 | }); | ||
| 836 | }); | ||
| 837 | |||
| 838 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 839 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 840 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 841 | }) | ||
| 842 | }); | ||
| 843 | |||
| 844 | shutdown_session.await.unwrap(); | ||
| 845 | } | ||
| @@ -0,0 +1,1759 @@ | |||
| 1 | use std::sync::{ | ||
| 2 | atomic::{AtomicBool, Ordering}, | ||
| 3 | Arc, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use crate::{ | ||
| 7 | session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry}, | ||
| 8 | tests::{active_debug_session_panel, init_test, init_test_workspace}, | ||
| 9 | DebugPanel, | ||
| 10 | }; | ||
| 11 | use collections::HashMap; | ||
| 12 | use dap::{ | ||
| 13 | requests::{Initialize, Launch, Scopes, StackTrace, Variables}, | ||
| 14 | Scope, StackFrame, Variable, | ||
| 15 | }; | ||
| 16 | use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; | ||
| 17 | use menu::{SelectFirst, SelectNext, SelectPrevious}; | ||
| 18 | use project::{FakeFs, Project}; | ||
| 19 | use serde_json::json; | ||
| 20 | use unindent::Unindent as _; | ||
| 21 | use util::path; | ||
| 22 | |||
| 23 | /// This only tests fetching one scope and 2 variables for a single stackframe | ||
| 24 | #[gpui::test] | ||
| 25 | async fn test_basic_fetch_initial_scope_and_variables( | ||
| 26 | executor: BackgroundExecutor, | ||
| 27 | cx: &mut TestAppContext, | ||
| 28 | ) { | ||
| 29 | init_test(cx); | ||
| 30 | |||
| 31 | let fs = FakeFs::new(executor.clone()); | ||
| 32 | |||
| 33 | let test_file_content = r#" | ||
| 34 | const variable1 = "Value 1"; | ||
| 35 | const variable2 = "Value 2"; | ||
| 36 | "# | ||
| 37 | .unindent(); | ||
| 38 | |||
| 39 | fs.insert_tree( | ||
| 40 | path!("/project"), | ||
| 41 | json!({ | ||
| 42 | "src": { | ||
| 43 | "test.js": test_file_content, | ||
| 44 | } | ||
| 45 | }), | ||
| 46 | ) | ||
| 47 | .await; | ||
| 48 | |||
| 49 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 50 | let workspace = init_test_workspace(&project, cx).await; | ||
| 51 | workspace | ||
| 52 | .update(cx, |workspace, window, cx| { | ||
| 53 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 54 | }) | ||
| 55 | .unwrap(); | ||
| 56 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 57 | |||
| 58 | let task = project.update(cx, |project, cx| { | ||
| 59 | project.start_debug_session( | ||
| 60 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 61 | cx, | ||
| 62 | ) | ||
| 63 | }); | ||
| 64 | |||
| 65 | let session = task.await.unwrap(); | ||
| 66 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 67 | |||
| 68 | client | ||
| 69 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 70 | Ok(dap::ThreadsResponse { | ||
| 71 | threads: vec![dap::Thread { | ||
| 72 | id: 1, | ||
| 73 | name: "Thread 1".into(), | ||
| 74 | }], | ||
| 75 | }) | ||
| 76 | }) | ||
| 77 | .await; | ||
| 78 | |||
| 79 | let stack_frames = vec![StackFrame { | ||
| 80 | id: 1, | ||
| 81 | name: "Stack Frame 1".into(), | ||
| 82 | source: Some(dap::Source { | ||
| 83 | name: Some("test.js".into()), | ||
| 84 | path: Some(path!("/project/src/test.js").into()), | ||
| 85 | source_reference: None, | ||
| 86 | presentation_hint: None, | ||
| 87 | origin: None, | ||
| 88 | sources: None, | ||
| 89 | adapter_data: None, | ||
| 90 | checksums: None, | ||
| 91 | }), | ||
| 92 | line: 1, | ||
| 93 | column: 1, | ||
| 94 | end_line: None, | ||
| 95 | end_column: None, | ||
| 96 | can_restart: None, | ||
| 97 | instruction_pointer_reference: None, | ||
| 98 | module_id: None, | ||
| 99 | presentation_hint: None, | ||
| 100 | }]; | ||
| 101 | |||
| 102 | client | ||
| 103 | .on_request::<StackTrace, _>({ | ||
| 104 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 105 | move |_, args| { | ||
| 106 | assert_eq!(1, args.thread_id); | ||
| 107 | |||
| 108 | Ok(dap::StackTraceResponse { | ||
| 109 | stack_frames: (*stack_frames).clone(), | ||
| 110 | total_frames: None, | ||
| 111 | }) | ||
| 112 | } | ||
| 113 | }) | ||
| 114 | .await; | ||
| 115 | |||
| 116 | let scopes = vec![Scope { | ||
| 117 | name: "Scope 1".into(), | ||
| 118 | presentation_hint: None, | ||
| 119 | variables_reference: 2, | ||
| 120 | named_variables: None, | ||
| 121 | indexed_variables: None, | ||
| 122 | expensive: false, | ||
| 123 | source: None, | ||
| 124 | line: None, | ||
| 125 | column: None, | ||
| 126 | end_line: None, | ||
| 127 | end_column: None, | ||
| 128 | }]; | ||
| 129 | |||
| 130 | client | ||
| 131 | .on_request::<Scopes, _>({ | ||
| 132 | let scopes = Arc::new(scopes.clone()); | ||
| 133 | move |_, args| { | ||
| 134 | assert_eq!(1, args.frame_id); | ||
| 135 | |||
| 136 | Ok(dap::ScopesResponse { | ||
| 137 | scopes: (*scopes).clone(), | ||
| 138 | }) | ||
| 139 | } | ||
| 140 | }) | ||
| 141 | .await; | ||
| 142 | |||
| 143 | let variables = vec![ | ||
| 144 | Variable { | ||
| 145 | name: "variable1".into(), | ||
| 146 | value: "value 1".into(), | ||
| 147 | type_: None, | ||
| 148 | presentation_hint: None, | ||
| 149 | evaluate_name: None, | ||
| 150 | variables_reference: 0, | ||
| 151 | named_variables: None, | ||
| 152 | indexed_variables: None, | ||
| 153 | memory_reference: None, | ||
| 154 | declaration_location_reference: None, | ||
| 155 | value_location_reference: None, | ||
| 156 | }, | ||
| 157 | Variable { | ||
| 158 | name: "variable2".into(), | ||
| 159 | value: "value 2".into(), | ||
| 160 | type_: None, | ||
| 161 | presentation_hint: None, | ||
| 162 | evaluate_name: None, | ||
| 163 | variables_reference: 0, | ||
| 164 | named_variables: None, | ||
| 165 | indexed_variables: None, | ||
| 166 | memory_reference: None, | ||
| 167 | declaration_location_reference: None, | ||
| 168 | value_location_reference: None, | ||
| 169 | }, | ||
| 170 | ]; | ||
| 171 | |||
| 172 | client | ||
| 173 | .on_request::<Variables, _>({ | ||
| 174 | let variables = Arc::new(variables.clone()); | ||
| 175 | move |_, args| { | ||
| 176 | assert_eq!(2, args.variables_reference); | ||
| 177 | |||
| 178 | Ok(dap::VariablesResponse { | ||
| 179 | variables: (*variables).clone(), | ||
| 180 | }) | ||
| 181 | } | ||
| 182 | }) | ||
| 183 | .await; | ||
| 184 | |||
| 185 | client | ||
| 186 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 187 | reason: dap::StoppedEventReason::Pause, | ||
| 188 | description: None, | ||
| 189 | thread_id: Some(1), | ||
| 190 | preserve_focus_hint: None, | ||
| 191 | text: None, | ||
| 192 | all_threads_stopped: None, | ||
| 193 | hit_breakpoint_ids: None, | ||
| 194 | })) | ||
| 195 | .await; | ||
| 196 | |||
| 197 | cx.run_until_parked(); | ||
| 198 | |||
| 199 | let running_state = | ||
| 200 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 201 | cx.focus_self(window); | ||
| 202 | item.mode() | ||
| 203 | .as_running() | ||
| 204 | .expect("Session should be running by this point") | ||
| 205 | .clone() | ||
| 206 | }); | ||
| 207 | |||
| 208 | cx.run_until_parked(); | ||
| 209 | |||
| 210 | running_state.update(cx, |running_state, cx| { | ||
| 211 | let (stack_frame_list, stack_frame_id) = | ||
| 212 | running_state.stack_frame_list().update(cx, |list, _| { | ||
| 213 | (list.flatten_entries(), list.current_stack_frame_id()) | ||
| 214 | }); | ||
| 215 | |||
| 216 | assert_eq!(stack_frames, stack_frame_list); | ||
| 217 | assert_eq!(Some(1), stack_frame_id); | ||
| 218 | |||
| 219 | running_state | ||
| 220 | .variable_list() | ||
| 221 | .update(cx, |variable_list, _| { | ||
| 222 | assert_eq!(1, variable_list.scopes().len()); | ||
| 223 | assert_eq!(scopes, variable_list.scopes()); | ||
| 224 | assert_eq!( | ||
| 225 | vec![variables[0].clone(), variables[1].clone(),], | ||
| 226 | variable_list.variables() | ||
| 227 | ); | ||
| 228 | |||
| 229 | variable_list.assert_visual_entries(vec![ | ||
| 230 | "v Scope 1", | ||
| 231 | " > variable1", | ||
| 232 | " > variable2", | ||
| 233 | ]); | ||
| 234 | }); | ||
| 235 | }); | ||
| 236 | |||
| 237 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 238 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 239 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 240 | }) | ||
| 241 | }); | ||
| 242 | |||
| 243 | shutdown_session.await.unwrap(); | ||
| 244 | } | ||
| 245 | |||
| 246 | /// This tests fetching multiple scopes and variables for them with a single stackframe | ||
| 247 | #[gpui::test] | ||
| 248 | async fn test_fetch_variables_for_multiple_scopes( | ||
| 249 | executor: BackgroundExecutor, | ||
| 250 | cx: &mut TestAppContext, | ||
| 251 | ) { | ||
| 252 | init_test(cx); | ||
| 253 | |||
| 254 | let fs = FakeFs::new(executor.clone()); | ||
| 255 | |||
| 256 | let test_file_content = r#" | ||
| 257 | const variable1 = { | ||
| 258 | nested1: "Nested 1", | ||
| 259 | nested2: "Nested 2", | ||
| 260 | }; | ||
| 261 | const variable2 = "Value 2"; | ||
| 262 | const variable3 = "Value 3"; | ||
| 263 | "# | ||
| 264 | .unindent(); | ||
| 265 | |||
| 266 | fs.insert_tree( | ||
| 267 | path!("/project"), | ||
| 268 | json!({ | ||
| 269 | "src": { | ||
| 270 | "test.js": test_file_content, | ||
| 271 | } | ||
| 272 | }), | ||
| 273 | ) | ||
| 274 | .await; | ||
| 275 | |||
| 276 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 277 | let workspace = init_test_workspace(&project, cx).await; | ||
| 278 | workspace | ||
| 279 | .update(cx, |workspace, window, cx| { | ||
| 280 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 281 | }) | ||
| 282 | .unwrap(); | ||
| 283 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 284 | |||
| 285 | let task = project.update(cx, |project, cx| { | ||
| 286 | project.start_debug_session( | ||
| 287 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 288 | cx, | ||
| 289 | ) | ||
| 290 | }); | ||
| 291 | |||
| 292 | let session = task.await.unwrap(); | ||
| 293 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 294 | |||
| 295 | client | ||
| 296 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 297 | Ok(dap::ThreadsResponse { | ||
| 298 | threads: vec![dap::Thread { | ||
| 299 | id: 1, | ||
| 300 | name: "Thread 1".into(), | ||
| 301 | }], | ||
| 302 | }) | ||
| 303 | }) | ||
| 304 | .await; | ||
| 305 | |||
| 306 | client | ||
| 307 | .on_request::<Initialize, _>(move |_, _| { | ||
| 308 | Ok(dap::Capabilities { | ||
| 309 | supports_step_back: Some(false), | ||
| 310 | ..Default::default() | ||
| 311 | }) | ||
| 312 | }) | ||
| 313 | .await; | ||
| 314 | |||
| 315 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 316 | |||
| 317 | let stack_frames = vec![StackFrame { | ||
| 318 | id: 1, | ||
| 319 | name: "Stack Frame 1".into(), | ||
| 320 | source: Some(dap::Source { | ||
| 321 | name: Some("test.js".into()), | ||
| 322 | path: Some(path!("/project/src/test.js").into()), | ||
| 323 | source_reference: None, | ||
| 324 | presentation_hint: None, | ||
| 325 | origin: None, | ||
| 326 | sources: None, | ||
| 327 | adapter_data: None, | ||
| 328 | checksums: None, | ||
| 329 | }), | ||
| 330 | line: 1, | ||
| 331 | column: 1, | ||
| 332 | end_line: None, | ||
| 333 | end_column: None, | ||
| 334 | can_restart: None, | ||
| 335 | instruction_pointer_reference: None, | ||
| 336 | module_id: None, | ||
| 337 | presentation_hint: None, | ||
| 338 | }]; | ||
| 339 | |||
| 340 | client | ||
| 341 | .on_request::<StackTrace, _>({ | ||
| 342 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 343 | move |_, args| { | ||
| 344 | assert_eq!(1, args.thread_id); | ||
| 345 | |||
| 346 | Ok(dap::StackTraceResponse { | ||
| 347 | stack_frames: (*stack_frames).clone(), | ||
| 348 | total_frames: None, | ||
| 349 | }) | ||
| 350 | } | ||
| 351 | }) | ||
| 352 | .await; | ||
| 353 | |||
| 354 | let scopes = vec![ | ||
| 355 | Scope { | ||
| 356 | name: "Scope 1".into(), | ||
| 357 | presentation_hint: Some(dap::ScopePresentationHint::Locals), | ||
| 358 | variables_reference: 2, | ||
| 359 | named_variables: None, | ||
| 360 | indexed_variables: None, | ||
| 361 | expensive: false, | ||
| 362 | source: None, | ||
| 363 | line: None, | ||
| 364 | column: None, | ||
| 365 | end_line: None, | ||
| 366 | end_column: None, | ||
| 367 | }, | ||
| 368 | Scope { | ||
| 369 | name: "Scope 2".into(), | ||
| 370 | presentation_hint: None, | ||
| 371 | variables_reference: 3, | ||
| 372 | named_variables: None, | ||
| 373 | indexed_variables: None, | ||
| 374 | expensive: false, | ||
| 375 | source: None, | ||
| 376 | line: None, | ||
| 377 | column: None, | ||
| 378 | end_line: None, | ||
| 379 | end_column: None, | ||
| 380 | }, | ||
| 381 | ]; | ||
| 382 | |||
| 383 | client | ||
| 384 | .on_request::<Scopes, _>({ | ||
| 385 | let scopes = Arc::new(scopes.clone()); | ||
| 386 | move |_, args| { | ||
| 387 | assert_eq!(1, args.frame_id); | ||
| 388 | |||
| 389 | Ok(dap::ScopesResponse { | ||
| 390 | scopes: (*scopes).clone(), | ||
| 391 | }) | ||
| 392 | } | ||
| 393 | }) | ||
| 394 | .await; | ||
| 395 | |||
| 396 | let mut variables = HashMap::default(); | ||
| 397 | variables.insert( | ||
| 398 | 2, | ||
| 399 | vec![ | ||
| 400 | Variable { | ||
| 401 | name: "variable1".into(), | ||
| 402 | value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), | ||
| 403 | type_: None, | ||
| 404 | presentation_hint: None, | ||
| 405 | evaluate_name: None, | ||
| 406 | variables_reference: 0, | ||
| 407 | named_variables: None, | ||
| 408 | indexed_variables: None, | ||
| 409 | memory_reference: None, | ||
| 410 | declaration_location_reference: None, | ||
| 411 | value_location_reference: None, | ||
| 412 | }, | ||
| 413 | Variable { | ||
| 414 | name: "variable2".into(), | ||
| 415 | value: "Value 2".into(), | ||
| 416 | type_: None, | ||
| 417 | presentation_hint: None, | ||
| 418 | evaluate_name: None, | ||
| 419 | variables_reference: 0, | ||
| 420 | named_variables: None, | ||
| 421 | indexed_variables: None, | ||
| 422 | memory_reference: None, | ||
| 423 | declaration_location_reference: None, | ||
| 424 | value_location_reference: None, | ||
| 425 | }, | ||
| 426 | ], | ||
| 427 | ); | ||
| 428 | variables.insert( | ||
| 429 | 3, | ||
| 430 | vec![Variable { | ||
| 431 | name: "variable3".into(), | ||
| 432 | value: "Value 3".into(), | ||
| 433 | type_: None, | ||
| 434 | presentation_hint: None, | ||
| 435 | evaluate_name: None, | ||
| 436 | variables_reference: 0, | ||
| 437 | named_variables: None, | ||
| 438 | indexed_variables: None, | ||
| 439 | memory_reference: None, | ||
| 440 | declaration_location_reference: None, | ||
| 441 | value_location_reference: None, | ||
| 442 | }], | ||
| 443 | ); | ||
| 444 | |||
| 445 | client | ||
| 446 | .on_request::<Variables, _>({ | ||
| 447 | let variables = Arc::new(variables.clone()); | ||
| 448 | move |_, args| { | ||
| 449 | Ok(dap::VariablesResponse { | ||
| 450 | variables: variables.get(&args.variables_reference).unwrap().clone(), | ||
| 451 | }) | ||
| 452 | } | ||
| 453 | }) | ||
| 454 | .await; | ||
| 455 | |||
| 456 | client | ||
| 457 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 458 | reason: dap::StoppedEventReason::Pause, | ||
| 459 | description: None, | ||
| 460 | thread_id: Some(1), | ||
| 461 | preserve_focus_hint: None, | ||
| 462 | text: None, | ||
| 463 | all_threads_stopped: None, | ||
| 464 | hit_breakpoint_ids: None, | ||
| 465 | })) | ||
| 466 | .await; | ||
| 467 | |||
| 468 | cx.run_until_parked(); | ||
| 469 | |||
| 470 | let running_state = | ||
| 471 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 472 | cx.focus_self(window); | ||
| 473 | item.mode() | ||
| 474 | .as_running() | ||
| 475 | .expect("Session should be running by this point") | ||
| 476 | .clone() | ||
| 477 | }); | ||
| 478 | |||
| 479 | cx.run_until_parked(); | ||
| 480 | |||
| 481 | running_state.update(cx, |running_state, cx| { | ||
| 482 | let (stack_frame_list, stack_frame_id) = | ||
| 483 | running_state.stack_frame_list().update(cx, |list, _| { | ||
| 484 | (list.flatten_entries(), list.current_stack_frame_id()) | ||
| 485 | }); | ||
| 486 | |||
| 487 | assert_eq!(Some(1), stack_frame_id); | ||
| 488 | assert_eq!(stack_frames, stack_frame_list); | ||
| 489 | |||
| 490 | running_state | ||
| 491 | .variable_list() | ||
| 492 | .update(cx, |variable_list, _| { | ||
| 493 | assert_eq!(2, variable_list.scopes().len()); | ||
| 494 | assert_eq!(scopes, variable_list.scopes()); | ||
| 495 | let variables_by_scope = variable_list.variables_per_scope(); | ||
| 496 | |||
| 497 | // scope 1 | ||
| 498 | assert_eq!( | ||
| 499 | vec![ | ||
| 500 | variables.get(&2).unwrap()[0].clone(), | ||
| 501 | variables.get(&2).unwrap()[1].clone(), | ||
| 502 | ], | ||
| 503 | variables_by_scope[0].1 | ||
| 504 | ); | ||
| 505 | |||
| 506 | // scope 2 | ||
| 507 | let empty_vec: Vec<dap::Variable> = vec![]; | ||
| 508 | assert_eq!(empty_vec, variables_by_scope[1].1); | ||
| 509 | |||
| 510 | variable_list.assert_visual_entries(vec![ | ||
| 511 | "v Scope 1", | ||
| 512 | " > variable1", | ||
| 513 | " > variable2", | ||
| 514 | "> Scope 2", | ||
| 515 | ]); | ||
| 516 | }); | ||
| 517 | }); | ||
| 518 | |||
| 519 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 520 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 521 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 522 | }) | ||
| 523 | }); | ||
| 524 | |||
| 525 | shutdown_session.await.unwrap(); | ||
| 526 | } | ||
| 527 | |||
| 528 | // tests that toggling a variable will fetch its children and shows it | ||
| 529 | #[gpui::test] | ||
| 530 | async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestAppContext) { | ||
| 531 | init_test(cx); | ||
| 532 | |||
| 533 | let fs = FakeFs::new(executor.clone()); | ||
| 534 | |||
| 535 | let test_file_content = r#" | ||
| 536 | const variable1 = { | ||
| 537 | nested1: "Nested 1", | ||
| 538 | nested2: "Nested 2", | ||
| 539 | }; | ||
| 540 | const variable2 = "Value 2"; | ||
| 541 | const variable3 = "Value 3"; | ||
| 542 | "# | ||
| 543 | .unindent(); | ||
| 544 | |||
| 545 | fs.insert_tree( | ||
| 546 | path!("/project"), | ||
| 547 | json!({ | ||
| 548 | "src": { | ||
| 549 | "test.js": test_file_content, | ||
| 550 | } | ||
| 551 | }), | ||
| 552 | ) | ||
| 553 | .await; | ||
| 554 | |||
| 555 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 556 | let workspace = init_test_workspace(&project, cx).await; | ||
| 557 | workspace | ||
| 558 | .update(cx, |workspace, window, cx| { | ||
| 559 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 560 | }) | ||
| 561 | .unwrap(); | ||
| 562 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 563 | |||
| 564 | let task = project.update(cx, |project, cx| { | ||
| 565 | project.start_debug_session( | ||
| 566 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 567 | cx, | ||
| 568 | ) | ||
| 569 | }); | ||
| 570 | |||
| 571 | let session = task.await.unwrap(); | ||
| 572 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 573 | |||
| 574 | client | ||
| 575 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 576 | Ok(dap::ThreadsResponse { | ||
| 577 | threads: vec![dap::Thread { | ||
| 578 | id: 1, | ||
| 579 | name: "Thread 1".into(), | ||
| 580 | }], | ||
| 581 | }) | ||
| 582 | }) | ||
| 583 | .await; | ||
| 584 | |||
| 585 | client | ||
| 586 | .on_request::<Initialize, _>(move |_, _| { | ||
| 587 | Ok(dap::Capabilities { | ||
| 588 | supports_step_back: Some(false), | ||
| 589 | ..Default::default() | ||
| 590 | }) | ||
| 591 | }) | ||
| 592 | .await; | ||
| 593 | |||
| 594 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 595 | |||
| 596 | let stack_frames = vec![StackFrame { | ||
| 597 | id: 1, | ||
| 598 | name: "Stack Frame 1".into(), | ||
| 599 | source: Some(dap::Source { | ||
| 600 | name: Some("test.js".into()), | ||
| 601 | path: Some(path!("/project/src/test.js").into()), | ||
| 602 | source_reference: None, | ||
| 603 | presentation_hint: None, | ||
| 604 | origin: None, | ||
| 605 | sources: None, | ||
| 606 | adapter_data: None, | ||
| 607 | checksums: None, | ||
| 608 | }), | ||
| 609 | line: 1, | ||
| 610 | column: 1, | ||
| 611 | end_line: None, | ||
| 612 | end_column: None, | ||
| 613 | can_restart: None, | ||
| 614 | instruction_pointer_reference: None, | ||
| 615 | module_id: None, | ||
| 616 | presentation_hint: None, | ||
| 617 | }]; | ||
| 618 | |||
| 619 | client | ||
| 620 | .on_request::<StackTrace, _>({ | ||
| 621 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 622 | move |_, args| { | ||
| 623 | assert_eq!(1, args.thread_id); | ||
| 624 | |||
| 625 | Ok(dap::StackTraceResponse { | ||
| 626 | stack_frames: (*stack_frames).clone(), | ||
| 627 | total_frames: None, | ||
| 628 | }) | ||
| 629 | } | ||
| 630 | }) | ||
| 631 | .await; | ||
| 632 | |||
| 633 | let scopes = vec![ | ||
| 634 | Scope { | ||
| 635 | name: "Scope 1".into(), | ||
| 636 | presentation_hint: Some(dap::ScopePresentationHint::Locals), | ||
| 637 | variables_reference: 2, | ||
| 638 | named_variables: None, | ||
| 639 | indexed_variables: None, | ||
| 640 | expensive: false, | ||
| 641 | source: None, | ||
| 642 | line: None, | ||
| 643 | column: None, | ||
| 644 | end_line: None, | ||
| 645 | end_column: None, | ||
| 646 | }, | ||
| 647 | Scope { | ||
| 648 | name: "Scope 2".into(), | ||
| 649 | presentation_hint: None, | ||
| 650 | variables_reference: 4, | ||
| 651 | named_variables: None, | ||
| 652 | indexed_variables: None, | ||
| 653 | expensive: false, | ||
| 654 | source: None, | ||
| 655 | line: None, | ||
| 656 | column: None, | ||
| 657 | end_line: None, | ||
| 658 | end_column: None, | ||
| 659 | }, | ||
| 660 | ]; | ||
| 661 | |||
| 662 | client | ||
| 663 | .on_request::<Scopes, _>({ | ||
| 664 | let scopes = Arc::new(scopes.clone()); | ||
| 665 | move |_, args| { | ||
| 666 | assert_eq!(1, args.frame_id); | ||
| 667 | |||
| 668 | Ok(dap::ScopesResponse { | ||
| 669 | scopes: (*scopes).clone(), | ||
| 670 | }) | ||
| 671 | } | ||
| 672 | }) | ||
| 673 | .await; | ||
| 674 | |||
| 675 | let scope1_variables = vec![ | ||
| 676 | Variable { | ||
| 677 | name: "variable1".into(), | ||
| 678 | value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), | ||
| 679 | type_: None, | ||
| 680 | presentation_hint: None, | ||
| 681 | evaluate_name: None, | ||
| 682 | variables_reference: 3, | ||
| 683 | named_variables: None, | ||
| 684 | indexed_variables: None, | ||
| 685 | memory_reference: None, | ||
| 686 | declaration_location_reference: None, | ||
| 687 | value_location_reference: None, | ||
| 688 | }, | ||
| 689 | Variable { | ||
| 690 | name: "variable2".into(), | ||
| 691 | value: "Value 2".into(), | ||
| 692 | type_: None, | ||
| 693 | presentation_hint: None, | ||
| 694 | evaluate_name: None, | ||
| 695 | variables_reference: 0, | ||
| 696 | named_variables: None, | ||
| 697 | indexed_variables: None, | ||
| 698 | memory_reference: None, | ||
| 699 | declaration_location_reference: None, | ||
| 700 | value_location_reference: None, | ||
| 701 | }, | ||
| 702 | ]; | ||
| 703 | |||
| 704 | let nested_variables = vec![ | ||
| 705 | Variable { | ||
| 706 | name: "nested1".into(), | ||
| 707 | value: "Nested 1".into(), | ||
| 708 | type_: None, | ||
| 709 | presentation_hint: None, | ||
| 710 | evaluate_name: None, | ||
| 711 | variables_reference: 0, | ||
| 712 | named_variables: None, | ||
| 713 | indexed_variables: None, | ||
| 714 | memory_reference: None, | ||
| 715 | declaration_location_reference: None, | ||
| 716 | value_location_reference: None, | ||
| 717 | }, | ||
| 718 | Variable { | ||
| 719 | name: "nested2".into(), | ||
| 720 | value: "Nested 2".into(), | ||
| 721 | type_: None, | ||
| 722 | presentation_hint: None, | ||
| 723 | evaluate_name: None, | ||
| 724 | variables_reference: 0, | ||
| 725 | named_variables: None, | ||
| 726 | indexed_variables: None, | ||
| 727 | memory_reference: None, | ||
| 728 | declaration_location_reference: None, | ||
| 729 | value_location_reference: None, | ||
| 730 | }, | ||
| 731 | ]; | ||
| 732 | |||
| 733 | let scope2_variables = vec![Variable { | ||
| 734 | name: "variable3".into(), | ||
| 735 | value: "Value 3".into(), | ||
| 736 | type_: None, | ||
| 737 | presentation_hint: None, | ||
| 738 | evaluate_name: None, | ||
| 739 | variables_reference: 0, | ||
| 740 | named_variables: None, | ||
| 741 | indexed_variables: None, | ||
| 742 | memory_reference: None, | ||
| 743 | declaration_location_reference: None, | ||
| 744 | value_location_reference: None, | ||
| 745 | }]; | ||
| 746 | |||
| 747 | client | ||
| 748 | .on_request::<Variables, _>({ | ||
| 749 | let scope1_variables = Arc::new(scope1_variables.clone()); | ||
| 750 | let nested_variables = Arc::new(nested_variables.clone()); | ||
| 751 | let scope2_variables = Arc::new(scope2_variables.clone()); | ||
| 752 | move |_, args| match args.variables_reference { | ||
| 753 | 4 => Ok(dap::VariablesResponse { | ||
| 754 | variables: (*scope2_variables).clone(), | ||
| 755 | }), | ||
| 756 | 3 => Ok(dap::VariablesResponse { | ||
| 757 | variables: (*nested_variables).clone(), | ||
| 758 | }), | ||
| 759 | 2 => Ok(dap::VariablesResponse { | ||
| 760 | variables: (*scope1_variables).clone(), | ||
| 761 | }), | ||
| 762 | id => unreachable!("unexpected variables reference {id}"), | ||
| 763 | } | ||
| 764 | }) | ||
| 765 | .await; | ||
| 766 | |||
| 767 | client | ||
| 768 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 769 | reason: dap::StoppedEventReason::Pause, | ||
| 770 | description: None, | ||
| 771 | thread_id: Some(1), | ||
| 772 | preserve_focus_hint: None, | ||
| 773 | text: None, | ||
| 774 | all_threads_stopped: None, | ||
| 775 | hit_breakpoint_ids: None, | ||
| 776 | })) | ||
| 777 | .await; | ||
| 778 | |||
| 779 | cx.run_until_parked(); | ||
| 780 | let running_state = | ||
| 781 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 782 | cx.focus_self(window); | ||
| 783 | let running = item | ||
| 784 | .mode() | ||
| 785 | .as_running() | ||
| 786 | .expect("Session should be running by this point") | ||
| 787 | .clone(); | ||
| 788 | |||
| 789 | let variable_list = running.read_with(cx, |state, _| state.variable_list().clone()); | ||
| 790 | variable_list.update(cx, |_, cx| cx.focus_self(window)); | ||
| 791 | running | ||
| 792 | }); | ||
| 793 | |||
| 794 | cx.dispatch_action(SelectFirst); | ||
| 795 | cx.run_until_parked(); | ||
| 796 | |||
| 797 | running_state.update(cx, |running_state, cx| { | ||
| 798 | running_state | ||
| 799 | .variable_list() | ||
| 800 | .update(cx, |variable_list, _| { | ||
| 801 | variable_list.assert_visual_entries(vec![ | ||
| 802 | "v Scope 1 <=== selected", | ||
| 803 | " > variable1", | ||
| 804 | " > variable2", | ||
| 805 | "> Scope 2", | ||
| 806 | ]); | ||
| 807 | }); | ||
| 808 | }); | ||
| 809 | |||
| 810 | cx.dispatch_action(SelectNext); | ||
| 811 | cx.run_until_parked(); | ||
| 812 | |||
| 813 | running_state.update(cx, |running_state, cx| { | ||
| 814 | running_state | ||
| 815 | .variable_list() | ||
| 816 | .update(cx, |variable_list, _| { | ||
| 817 | variable_list.assert_visual_entries(vec![ | ||
| 818 | "v Scope 1", | ||
| 819 | " > variable1 <=== selected", | ||
| 820 | " > variable2", | ||
| 821 | "> Scope 2", | ||
| 822 | ]); | ||
| 823 | }); | ||
| 824 | }); | ||
| 825 | |||
| 826 | // expand the nested variables of variable 1 | ||
| 827 | cx.dispatch_action(ExpandSelectedEntry); | ||
| 828 | cx.run_until_parked(); | ||
| 829 | running_state.update(cx, |running_state, cx| { | ||
| 830 | running_state | ||
| 831 | .variable_list() | ||
| 832 | .update(cx, |variable_list, _| { | ||
| 833 | variable_list.assert_visual_entries(vec![ | ||
| 834 | "v Scope 1", | ||
| 835 | " v variable1 <=== selected", | ||
| 836 | " > nested1", | ||
| 837 | " > nested2", | ||
| 838 | " > variable2", | ||
| 839 | "> Scope 2", | ||
| 840 | ]); | ||
| 841 | }); | ||
| 842 | }); | ||
| 843 | |||
| 844 | // select the first nested variable of variable 1 | ||
| 845 | cx.dispatch_action(SelectNext); | ||
| 846 | cx.run_until_parked(); | ||
| 847 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 848 | debug_panel_item | ||
| 849 | .variable_list() | ||
| 850 | .update(cx, |variable_list, _| { | ||
| 851 | variable_list.assert_visual_entries(vec![ | ||
| 852 | "v Scope 1", | ||
| 853 | " v variable1", | ||
| 854 | " > nested1 <=== selected", | ||
| 855 | " > nested2", | ||
| 856 | " > variable2", | ||
| 857 | "> Scope 2", | ||
| 858 | ]); | ||
| 859 | }); | ||
| 860 | }); | ||
| 861 | |||
| 862 | // select the second nested variable of variable 1 | ||
| 863 | cx.dispatch_action(SelectNext); | ||
| 864 | cx.run_until_parked(); | ||
| 865 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 866 | debug_panel_item | ||
| 867 | .variable_list() | ||
| 868 | .update(cx, |variable_list, _| { | ||
| 869 | variable_list.assert_visual_entries(vec![ | ||
| 870 | "v Scope 1", | ||
| 871 | " v variable1", | ||
| 872 | " > nested1", | ||
| 873 | " > nested2 <=== selected", | ||
| 874 | " > variable2", | ||
| 875 | "> Scope 2", | ||
| 876 | ]); | ||
| 877 | }); | ||
| 878 | }); | ||
| 879 | |||
| 880 | // select variable 2 of scope 1 | ||
| 881 | cx.dispatch_action(SelectNext); | ||
| 882 | cx.run_until_parked(); | ||
| 883 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 884 | debug_panel_item | ||
| 885 | .variable_list() | ||
| 886 | .update(cx, |variable_list, _| { | ||
| 887 | variable_list.assert_visual_entries(vec![ | ||
| 888 | "v Scope 1", | ||
| 889 | " v variable1", | ||
| 890 | " > nested1", | ||
| 891 | " > nested2", | ||
| 892 | " > variable2 <=== selected", | ||
| 893 | "> Scope 2", | ||
| 894 | ]); | ||
| 895 | }); | ||
| 896 | }); | ||
| 897 | |||
| 898 | // select scope 2 | ||
| 899 | cx.dispatch_action(SelectNext); | ||
| 900 | cx.run_until_parked(); | ||
| 901 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 902 | debug_panel_item | ||
| 903 | .variable_list() | ||
| 904 | .update(cx, |variable_list, _| { | ||
| 905 | variable_list.assert_visual_entries(vec![ | ||
| 906 | "v Scope 1", | ||
| 907 | " v variable1", | ||
| 908 | " > nested1", | ||
| 909 | " > nested2", | ||
| 910 | " > variable2", | ||
| 911 | "> Scope 2 <=== selected", | ||
| 912 | ]); | ||
| 913 | }); | ||
| 914 | }); | ||
| 915 | |||
| 916 | // expand the nested variables of scope 2 | ||
| 917 | cx.dispatch_action(ExpandSelectedEntry); | ||
| 918 | cx.run_until_parked(); | ||
| 919 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 920 | debug_panel_item | ||
| 921 | .variable_list() | ||
| 922 | .update(cx, |variable_list, _| { | ||
| 923 | variable_list.assert_visual_entries(vec![ | ||
| 924 | "v Scope 1", | ||
| 925 | " v variable1", | ||
| 926 | " > nested1", | ||
| 927 | " > nested2", | ||
| 928 | " > variable2", | ||
| 929 | "v Scope 2 <=== selected", | ||
| 930 | " > variable3", | ||
| 931 | ]); | ||
| 932 | }); | ||
| 933 | }); | ||
| 934 | |||
| 935 | // select variable 3 of scope 2 | ||
| 936 | cx.dispatch_action(SelectNext); | ||
| 937 | cx.run_until_parked(); | ||
| 938 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 939 | debug_panel_item | ||
| 940 | .variable_list() | ||
| 941 | .update(cx, |variable_list, _| { | ||
| 942 | variable_list.assert_visual_entries(vec![ | ||
| 943 | "v Scope 1", | ||
| 944 | " v variable1", | ||
| 945 | " > nested1", | ||
| 946 | " > nested2", | ||
| 947 | " > variable2", | ||
| 948 | "v Scope 2", | ||
| 949 | " > variable3 <=== selected", | ||
| 950 | ]); | ||
| 951 | }); | ||
| 952 | }); | ||
| 953 | |||
| 954 | // select scope 2 | ||
| 955 | cx.dispatch_action(SelectPrevious); | ||
| 956 | cx.run_until_parked(); | ||
| 957 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 958 | debug_panel_item | ||
| 959 | .variable_list() | ||
| 960 | .update(cx, |variable_list, _| { | ||
| 961 | variable_list.assert_visual_entries(vec![ | ||
| 962 | "v Scope 1", | ||
| 963 | " v variable1", | ||
| 964 | " > nested1", | ||
| 965 | " > nested2", | ||
| 966 | " > variable2", | ||
| 967 | "v Scope 2 <=== selected", | ||
| 968 | " > variable3", | ||
| 969 | ]); | ||
| 970 | }); | ||
| 971 | }); | ||
| 972 | |||
| 973 | // collapse variables of scope 2 | ||
| 974 | cx.dispatch_action(CollapseSelectedEntry); | ||
| 975 | cx.run_until_parked(); | ||
| 976 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 977 | debug_panel_item | ||
| 978 | .variable_list() | ||
| 979 | .update(cx, |variable_list, _| { | ||
| 980 | variable_list.assert_visual_entries(vec![ | ||
| 981 | "v Scope 1", | ||
| 982 | " v variable1", | ||
| 983 | " > nested1", | ||
| 984 | " > nested2", | ||
| 985 | " > variable2", | ||
| 986 | "> Scope 2 <=== selected", | ||
| 987 | ]); | ||
| 988 | }); | ||
| 989 | }); | ||
| 990 | |||
| 991 | // select variable 2 of scope 1 | ||
| 992 | cx.dispatch_action(SelectPrevious); | ||
| 993 | cx.run_until_parked(); | ||
| 994 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 995 | debug_panel_item | ||
| 996 | .variable_list() | ||
| 997 | .update(cx, |variable_list, _| { | ||
| 998 | variable_list.assert_visual_entries(vec![ | ||
| 999 | "v Scope 1", | ||
| 1000 | " v variable1", | ||
| 1001 | " > nested1", | ||
| 1002 | " > nested2", | ||
| 1003 | " > variable2 <=== selected", | ||
| 1004 | "> Scope 2", | ||
| 1005 | ]); | ||
| 1006 | }); | ||
| 1007 | }); | ||
| 1008 | |||
| 1009 | // select nested2 of variable 1 | ||
| 1010 | cx.dispatch_action(SelectPrevious); | ||
| 1011 | cx.run_until_parked(); | ||
| 1012 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 1013 | debug_panel_item | ||
| 1014 | .variable_list() | ||
| 1015 | .update(cx, |variable_list, _| { | ||
| 1016 | variable_list.assert_visual_entries(vec![ | ||
| 1017 | "v Scope 1", | ||
| 1018 | " v variable1", | ||
| 1019 | " > nested1", | ||
| 1020 | " > nested2 <=== selected", | ||
| 1021 | " > variable2", | ||
| 1022 | "> Scope 2", | ||
| 1023 | ]); | ||
| 1024 | }); | ||
| 1025 | }); | ||
| 1026 | |||
| 1027 | // select nested1 of variable 1 | ||
| 1028 | cx.dispatch_action(SelectPrevious); | ||
| 1029 | cx.run_until_parked(); | ||
| 1030 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 1031 | debug_panel_item | ||
| 1032 | .variable_list() | ||
| 1033 | .update(cx, |variable_list, _| { | ||
| 1034 | variable_list.assert_visual_entries(vec![ | ||
| 1035 | "v Scope 1", | ||
| 1036 | " v variable1", | ||
| 1037 | " > nested1 <=== selected", | ||
| 1038 | " > nested2", | ||
| 1039 | " > variable2", | ||
| 1040 | "> Scope 2", | ||
| 1041 | ]); | ||
| 1042 | }); | ||
| 1043 | }); | ||
| 1044 | |||
| 1045 | // select variable 1 of scope 1 | ||
| 1046 | cx.dispatch_action(SelectPrevious); | ||
| 1047 | cx.run_until_parked(); | ||
| 1048 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 1049 | debug_panel_item | ||
| 1050 | .variable_list() | ||
| 1051 | .update(cx, |variable_list, _| { | ||
| 1052 | variable_list.assert_visual_entries(vec![ | ||
| 1053 | "v Scope 1", | ||
| 1054 | " v variable1 <=== selected", | ||
| 1055 | " > nested1", | ||
| 1056 | " > nested2", | ||
| 1057 | " > variable2", | ||
| 1058 | "> Scope 2", | ||
| 1059 | ]); | ||
| 1060 | }); | ||
| 1061 | }); | ||
| 1062 | |||
| 1063 | // collapse variables of variable 1 | ||
| 1064 | cx.dispatch_action(CollapseSelectedEntry); | ||
| 1065 | cx.run_until_parked(); | ||
| 1066 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 1067 | debug_panel_item | ||
| 1068 | .variable_list() | ||
| 1069 | .update(cx, |variable_list, _| { | ||
| 1070 | variable_list.assert_visual_entries(vec![ | ||
| 1071 | "v Scope 1", | ||
| 1072 | " > variable1 <=== selected", | ||
| 1073 | " > variable2", | ||
| 1074 | "> Scope 2", | ||
| 1075 | ]); | ||
| 1076 | }); | ||
| 1077 | }); | ||
| 1078 | |||
| 1079 | // select scope 1 | ||
| 1080 | cx.dispatch_action(SelectPrevious); | ||
| 1081 | cx.run_until_parked(); | ||
| 1082 | running_state.update(cx, |running_state, cx| { | ||
| 1083 | running_state | ||
| 1084 | .variable_list() | ||
| 1085 | .update(cx, |variable_list, _| { | ||
| 1086 | variable_list.assert_visual_entries(vec![ | ||
| 1087 | "v Scope 1 <=== selected", | ||
| 1088 | " > variable1", | ||
| 1089 | " > variable2", | ||
| 1090 | "> Scope 2", | ||
| 1091 | ]); | ||
| 1092 | }); | ||
| 1093 | }); | ||
| 1094 | |||
| 1095 | // collapse variables of scope 1 | ||
| 1096 | cx.dispatch_action(CollapseSelectedEntry); | ||
| 1097 | cx.run_until_parked(); | ||
| 1098 | running_state.update(cx, |debug_panel_item, cx| { | ||
| 1099 | debug_panel_item | ||
| 1100 | .variable_list() | ||
| 1101 | .update(cx, |variable_list, _| { | ||
| 1102 | variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]); | ||
| 1103 | }); | ||
| 1104 | }); | ||
| 1105 | |||
| 1106 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 1107 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 1108 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 1109 | }) | ||
| 1110 | }); | ||
| 1111 | |||
| 1112 | shutdown_session.await.unwrap(); | ||
| 1113 | } | ||
| 1114 | |||
| 1115 | #[gpui::test] | ||
| 1116 | async fn test_variable_list_only_sends_requests_when_rendering( | ||
| 1117 | executor: BackgroundExecutor, | ||
| 1118 | cx: &mut TestAppContext, | ||
| 1119 | ) { | ||
| 1120 | init_test(cx); | ||
| 1121 | |||
| 1122 | let fs = FakeFs::new(executor.clone()); | ||
| 1123 | |||
| 1124 | let test_file_content = r#" | ||
| 1125 | import { SOME_VALUE } './module.js'; | ||
| 1126 | |||
| 1127 | console.log(SOME_VALUE); | ||
| 1128 | "# | ||
| 1129 | .unindent(); | ||
| 1130 | |||
| 1131 | let module_file_content = r#" | ||
| 1132 | export SOME_VALUE = 'some value'; | ||
| 1133 | "# | ||
| 1134 | .unindent(); | ||
| 1135 | |||
| 1136 | fs.insert_tree( | ||
| 1137 | path!("/project"), | ||
| 1138 | json!({ | ||
| 1139 | "src": { | ||
| 1140 | "test.js": test_file_content, | ||
| 1141 | "module.js": module_file_content, | ||
| 1142 | } | ||
| 1143 | }), | ||
| 1144 | ) | ||
| 1145 | .await; | ||
| 1146 | |||
| 1147 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 1148 | let workspace = init_test_workspace(&project, cx).await; | ||
| 1149 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 1150 | |||
| 1151 | let task = project.update(cx, |project, cx| { | ||
| 1152 | project.start_debug_session( | ||
| 1153 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 1154 | cx, | ||
| 1155 | ) | ||
| 1156 | }); | ||
| 1157 | |||
| 1158 | let session = task.await.unwrap(); | ||
| 1159 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 1160 | |||
| 1161 | client | ||
| 1162 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 1163 | Ok(dap::ThreadsResponse { | ||
| 1164 | threads: vec![dap::Thread { | ||
| 1165 | id: 1, | ||
| 1166 | name: "Thread 1".into(), | ||
| 1167 | }], | ||
| 1168 | }) | ||
| 1169 | }) | ||
| 1170 | .await; | ||
| 1171 | |||
| 1172 | client | ||
| 1173 | .on_request::<Initialize, _>(move |_, _| { | ||
| 1174 | Ok(dap::Capabilities { | ||
| 1175 | supports_step_back: Some(false), | ||
| 1176 | ..Default::default() | ||
| 1177 | }) | ||
| 1178 | }) | ||
| 1179 | .await; | ||
| 1180 | |||
| 1181 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 1182 | |||
| 1183 | let stack_frames = vec![ | ||
| 1184 | StackFrame { | ||
| 1185 | id: 1, | ||
| 1186 | name: "Stack Frame 1".into(), | ||
| 1187 | source: Some(dap::Source { | ||
| 1188 | name: Some("test.js".into()), | ||
| 1189 | path: Some(path!("/project/src/test.js").into()), | ||
| 1190 | source_reference: None, | ||
| 1191 | presentation_hint: None, | ||
| 1192 | origin: None, | ||
| 1193 | sources: None, | ||
| 1194 | adapter_data: None, | ||
| 1195 | checksums: None, | ||
| 1196 | }), | ||
| 1197 | line: 3, | ||
| 1198 | column: 1, | ||
| 1199 | end_line: None, | ||
| 1200 | end_column: None, | ||
| 1201 | can_restart: None, | ||
| 1202 | instruction_pointer_reference: None, | ||
| 1203 | module_id: None, | ||
| 1204 | presentation_hint: None, | ||
| 1205 | }, | ||
| 1206 | StackFrame { | ||
| 1207 | id: 2, | ||
| 1208 | name: "Stack Frame 2".into(), | ||
| 1209 | source: Some(dap::Source { | ||
| 1210 | name: Some("module.js".into()), | ||
| 1211 | path: Some(path!("/project/src/module.js").into()), | ||
| 1212 | source_reference: None, | ||
| 1213 | presentation_hint: None, | ||
| 1214 | origin: None, | ||
| 1215 | sources: None, | ||
| 1216 | adapter_data: None, | ||
| 1217 | checksums: None, | ||
| 1218 | }), | ||
| 1219 | line: 1, | ||
| 1220 | column: 1, | ||
| 1221 | end_line: None, | ||
| 1222 | end_column: None, | ||
| 1223 | can_restart: None, | ||
| 1224 | instruction_pointer_reference: None, | ||
| 1225 | module_id: None, | ||
| 1226 | presentation_hint: None, | ||
| 1227 | }, | ||
| 1228 | ]; | ||
| 1229 | |||
| 1230 | client | ||
| 1231 | .on_request::<StackTrace, _>({ | ||
| 1232 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 1233 | move |_, args| { | ||
| 1234 | assert_eq!(1, args.thread_id); | ||
| 1235 | |||
| 1236 | Ok(dap::StackTraceResponse { | ||
| 1237 | stack_frames: (*stack_frames).clone(), | ||
| 1238 | total_frames: None, | ||
| 1239 | }) | ||
| 1240 | } | ||
| 1241 | }) | ||
| 1242 | .await; | ||
| 1243 | |||
| 1244 | let frame_1_scopes = vec![Scope { | ||
| 1245 | name: "Frame 1 Scope 1".into(), | ||
| 1246 | presentation_hint: None, | ||
| 1247 | variables_reference: 2, | ||
| 1248 | named_variables: None, | ||
| 1249 | indexed_variables: None, | ||
| 1250 | expensive: false, | ||
| 1251 | source: None, | ||
| 1252 | line: None, | ||
| 1253 | column: None, | ||
| 1254 | end_line: None, | ||
| 1255 | end_column: None, | ||
| 1256 | }]; | ||
| 1257 | |||
| 1258 | let made_scopes_request = Arc::new(AtomicBool::new(false)); | ||
| 1259 | |||
| 1260 | client | ||
| 1261 | .on_request::<Scopes, _>({ | ||
| 1262 | let frame_1_scopes = Arc::new(frame_1_scopes.clone()); | ||
| 1263 | let made_scopes_request = made_scopes_request.clone(); | ||
| 1264 | move |_, args| { | ||
| 1265 | assert_eq!(1, args.frame_id); | ||
| 1266 | assert!( | ||
| 1267 | !made_scopes_request.load(Ordering::SeqCst), | ||
| 1268 | "We should be caching the scope request" | ||
| 1269 | ); | ||
| 1270 | |||
| 1271 | made_scopes_request.store(true, Ordering::SeqCst); | ||
| 1272 | |||
| 1273 | Ok(dap::ScopesResponse { | ||
| 1274 | scopes: (*frame_1_scopes).clone(), | ||
| 1275 | }) | ||
| 1276 | } | ||
| 1277 | }) | ||
| 1278 | .await; | ||
| 1279 | |||
| 1280 | let frame_1_variables = vec![ | ||
| 1281 | Variable { | ||
| 1282 | name: "variable1".into(), | ||
| 1283 | value: "value 1".into(), | ||
| 1284 | type_: None, | ||
| 1285 | presentation_hint: None, | ||
| 1286 | evaluate_name: None, | ||
| 1287 | variables_reference: 0, | ||
| 1288 | named_variables: None, | ||
| 1289 | indexed_variables: None, | ||
| 1290 | memory_reference: None, | ||
| 1291 | declaration_location_reference: None, | ||
| 1292 | value_location_reference: None, | ||
| 1293 | }, | ||
| 1294 | Variable { | ||
| 1295 | name: "variable2".into(), | ||
| 1296 | value: "value 2".into(), | ||
| 1297 | type_: None, | ||
| 1298 | presentation_hint: None, | ||
| 1299 | evaluate_name: None, | ||
| 1300 | variables_reference: 0, | ||
| 1301 | named_variables: None, | ||
| 1302 | indexed_variables: None, | ||
| 1303 | memory_reference: None, | ||
| 1304 | declaration_location_reference: None, | ||
| 1305 | value_location_reference: None, | ||
| 1306 | }, | ||
| 1307 | ]; | ||
| 1308 | |||
| 1309 | client | ||
| 1310 | .on_request::<Variables, _>({ | ||
| 1311 | let frame_1_variables = Arc::new(frame_1_variables.clone()); | ||
| 1312 | move |_, args| { | ||
| 1313 | assert_eq!(2, args.variables_reference); | ||
| 1314 | |||
| 1315 | Ok(dap::VariablesResponse { | ||
| 1316 | variables: (*frame_1_variables).clone(), | ||
| 1317 | }) | ||
| 1318 | } | ||
| 1319 | }) | ||
| 1320 | .await; | ||
| 1321 | |||
| 1322 | let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, cx| { | ||
| 1323 | let state = item | ||
| 1324 | .mode() | ||
| 1325 | .as_running() | ||
| 1326 | .expect("Session should be running by this point") | ||
| 1327 | .clone(); | ||
| 1328 | |||
| 1329 | state.update(cx, |state, cx| { | ||
| 1330 | state.set_thread_item(crate::session::ThreadItem::Modules, cx) | ||
| 1331 | }); | ||
| 1332 | state | ||
| 1333 | }); | ||
| 1334 | |||
| 1335 | client | ||
| 1336 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 1337 | reason: dap::StoppedEventReason::Pause, | ||
| 1338 | description: None, | ||
| 1339 | thread_id: Some(1), | ||
| 1340 | preserve_focus_hint: None, | ||
| 1341 | text: None, | ||
| 1342 | all_threads_stopped: None, | ||
| 1343 | hit_breakpoint_ids: None, | ||
| 1344 | })) | ||
| 1345 | .await; | ||
| 1346 | |||
| 1347 | cx.run_until_parked(); | ||
| 1348 | |||
| 1349 | // We shouldn't make any variable requests unless we're rendering the variable list | ||
| 1350 | running_state.update_in(cx, |running_state, window, cx| { | ||
| 1351 | let variable_list = running_state.variable_list().read(cx); | ||
| 1352 | let empty: Vec<dap::Variable> = vec![]; | ||
| 1353 | |||
| 1354 | assert_eq!(empty, variable_list.variables()); | ||
| 1355 | assert!(!made_scopes_request.load(Ordering::SeqCst)); | ||
| 1356 | |||
| 1357 | cx.focus_self(window); | ||
| 1358 | running_state.set_thread_item(crate::session::ThreadItem::Variables, cx); | ||
| 1359 | }); | ||
| 1360 | |||
| 1361 | cx.run_until_parked(); | ||
| 1362 | |||
| 1363 | running_state.update(cx, |running_state, cx| { | ||
| 1364 | let (stack_frame_list, stack_frame_id) = | ||
| 1365 | running_state.stack_frame_list().update(cx, |list, _| { | ||
| 1366 | (list.flatten_entries(), list.current_stack_frame_id()) | ||
| 1367 | }); | ||
| 1368 | |||
| 1369 | assert_eq!(Some(1), stack_frame_id); | ||
| 1370 | assert_eq!(stack_frames, stack_frame_list); | ||
| 1371 | |||
| 1372 | let variable_list = running_state.variable_list().read(cx); | ||
| 1373 | |||
| 1374 | assert_eq!(frame_1_variables, variable_list.variables()); | ||
| 1375 | assert!(made_scopes_request.load(Ordering::SeqCst)); | ||
| 1376 | }); | ||
| 1377 | |||
| 1378 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 1379 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 1380 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 1381 | }) | ||
| 1382 | }); | ||
| 1383 | |||
| 1384 | shutdown_session.await.unwrap(); | ||
| 1385 | } | ||
| 1386 | |||
| 1387 | #[gpui::test] | ||
| 1388 | async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( | ||
| 1389 | executor: BackgroundExecutor, | ||
| 1390 | cx: &mut TestAppContext, | ||
| 1391 | ) { | ||
| 1392 | init_test(cx); | ||
| 1393 | |||
| 1394 | let fs = FakeFs::new(executor.clone()); | ||
| 1395 | |||
| 1396 | let test_file_content = r#" | ||
| 1397 | import { SOME_VALUE } './module.js'; | ||
| 1398 | |||
| 1399 | console.log(SOME_VALUE); | ||
| 1400 | "# | ||
| 1401 | .unindent(); | ||
| 1402 | |||
| 1403 | let module_file_content = r#" | ||
| 1404 | export SOME_VALUE = 'some value'; | ||
| 1405 | "# | ||
| 1406 | .unindent(); | ||
| 1407 | |||
| 1408 | fs.insert_tree( | ||
| 1409 | path!("/project"), | ||
| 1410 | json!({ | ||
| 1411 | "src": { | ||
| 1412 | "test.js": test_file_content, | ||
| 1413 | "module.js": module_file_content, | ||
| 1414 | } | ||
| 1415 | }), | ||
| 1416 | ) | ||
| 1417 | .await; | ||
| 1418 | |||
| 1419 | let project = Project::test(fs, [path!("/project").as_ref()], cx).await; | ||
| 1420 | let workspace = init_test_workspace(&project, cx).await; | ||
| 1421 | workspace | ||
| 1422 | .update(cx, |workspace, window, cx| { | ||
| 1423 | workspace.focus_panel::<DebugPanel>(window, cx); | ||
| 1424 | }) | ||
| 1425 | .unwrap(); | ||
| 1426 | let cx = &mut VisualTestContext::from_window(*workspace, cx); | ||
| 1427 | |||
| 1428 | let task = project.update(cx, |project, cx| { | ||
| 1429 | project.start_debug_session( | ||
| 1430 | dap::test_config(dap::DebugRequestType::Launch, None, None), | ||
| 1431 | cx, | ||
| 1432 | ) | ||
| 1433 | }); | ||
| 1434 | |||
| 1435 | let session = task.await.unwrap(); | ||
| 1436 | let client = session.update(cx, |session, _| session.adapter_client().unwrap()); | ||
| 1437 | |||
| 1438 | client | ||
| 1439 | .on_request::<dap::requests::Threads, _>(move |_, _| { | ||
| 1440 | Ok(dap::ThreadsResponse { | ||
| 1441 | threads: vec![dap::Thread { | ||
| 1442 | id: 1, | ||
| 1443 | name: "Thread 1".into(), | ||
| 1444 | }], | ||
| 1445 | }) | ||
| 1446 | }) | ||
| 1447 | .await; | ||
| 1448 | |||
| 1449 | client | ||
| 1450 | .on_request::<Initialize, _>(move |_, _| { | ||
| 1451 | Ok(dap::Capabilities { | ||
| 1452 | supports_step_back: Some(false), | ||
| 1453 | ..Default::default() | ||
| 1454 | }) | ||
| 1455 | }) | ||
| 1456 | .await; | ||
| 1457 | |||
| 1458 | client.on_request::<Launch, _>(move |_, _| Ok(())).await; | ||
| 1459 | |||
| 1460 | let stack_frames = vec![ | ||
| 1461 | StackFrame { | ||
| 1462 | id: 1, | ||
| 1463 | name: "Stack Frame 1".into(), | ||
| 1464 | source: Some(dap::Source { | ||
| 1465 | name: Some("test.js".into()), | ||
| 1466 | path: Some(path!("/project/src/test.js").into()), | ||
| 1467 | source_reference: None, | ||
| 1468 | presentation_hint: None, | ||
| 1469 | origin: None, | ||
| 1470 | sources: None, | ||
| 1471 | adapter_data: None, | ||
| 1472 | checksums: None, | ||
| 1473 | }), | ||
| 1474 | line: 3, | ||
| 1475 | column: 1, | ||
| 1476 | end_line: None, | ||
| 1477 | end_column: None, | ||
| 1478 | can_restart: None, | ||
| 1479 | instruction_pointer_reference: None, | ||
| 1480 | module_id: None, | ||
| 1481 | presentation_hint: None, | ||
| 1482 | }, | ||
| 1483 | StackFrame { | ||
| 1484 | id: 2, | ||
| 1485 | name: "Stack Frame 2".into(), | ||
| 1486 | source: Some(dap::Source { | ||
| 1487 | name: Some("module.js".into()), | ||
| 1488 | path: Some(path!("/project/src/module.js").into()), | ||
| 1489 | source_reference: None, | ||
| 1490 | presentation_hint: None, | ||
| 1491 | origin: None, | ||
| 1492 | sources: None, | ||
| 1493 | adapter_data: None, | ||
| 1494 | checksums: None, | ||
| 1495 | }), | ||
| 1496 | line: 1, | ||
| 1497 | column: 1, | ||
| 1498 | end_line: None, | ||
| 1499 | end_column: None, | ||
| 1500 | can_restart: None, | ||
| 1501 | instruction_pointer_reference: None, | ||
| 1502 | module_id: None, | ||
| 1503 | presentation_hint: None, | ||
| 1504 | }, | ||
| 1505 | ]; | ||
| 1506 | |||
| 1507 | client | ||
| 1508 | .on_request::<StackTrace, _>({ | ||
| 1509 | let stack_frames = Arc::new(stack_frames.clone()); | ||
| 1510 | move |_, args| { | ||
| 1511 | assert_eq!(1, args.thread_id); | ||
| 1512 | |||
| 1513 | Ok(dap::StackTraceResponse { | ||
| 1514 | stack_frames: (*stack_frames).clone(), | ||
| 1515 | total_frames: None, | ||
| 1516 | }) | ||
| 1517 | } | ||
| 1518 | }) | ||
| 1519 | .await; | ||
| 1520 | |||
| 1521 | let frame_1_scopes = vec![Scope { | ||
| 1522 | name: "Frame 1 Scope 1".into(), | ||
| 1523 | presentation_hint: None, | ||
| 1524 | variables_reference: 2, | ||
| 1525 | named_variables: None, | ||
| 1526 | indexed_variables: None, | ||
| 1527 | expensive: false, | ||
| 1528 | source: None, | ||
| 1529 | line: None, | ||
| 1530 | column: None, | ||
| 1531 | end_line: None, | ||
| 1532 | end_column: None, | ||
| 1533 | }]; | ||
| 1534 | |||
| 1535 | // add handlers for fetching the second stack frame's scopes and variables | ||
| 1536 | // after the user clicked the stack frame | ||
| 1537 | let frame_2_scopes = vec![Scope { | ||
| 1538 | name: "Frame 2 Scope 1".into(), | ||
| 1539 | presentation_hint: None, | ||
| 1540 | variables_reference: 3, | ||
| 1541 | named_variables: None, | ||
| 1542 | indexed_variables: None, | ||
| 1543 | expensive: false, | ||
| 1544 | source: None, | ||
| 1545 | line: None, | ||
| 1546 | column: None, | ||
| 1547 | end_line: None, | ||
| 1548 | end_column: None, | ||
| 1549 | }]; | ||
| 1550 | |||
| 1551 | let called_second_stack_frame = Arc::new(AtomicBool::new(false)); | ||
| 1552 | let called_first_stack_frame = Arc::new(AtomicBool::new(false)); | ||
| 1553 | |||
| 1554 | client | ||
| 1555 | .on_request::<Scopes, _>({ | ||
| 1556 | let frame_1_scopes = Arc::new(frame_1_scopes.clone()); | ||
| 1557 | let frame_2_scopes = Arc::new(frame_2_scopes.clone()); | ||
| 1558 | let called_first_stack_frame = called_first_stack_frame.clone(); | ||
| 1559 | let called_second_stack_frame = called_second_stack_frame.clone(); | ||
| 1560 | move |_, args| match args.frame_id { | ||
| 1561 | 1 => { | ||
| 1562 | called_first_stack_frame.store(true, Ordering::SeqCst); | ||
| 1563 | Ok(dap::ScopesResponse { | ||
| 1564 | scopes: (*frame_1_scopes).clone(), | ||
| 1565 | }) | ||
| 1566 | } | ||
| 1567 | 2 => { | ||
| 1568 | called_second_stack_frame.store(true, Ordering::SeqCst); | ||
| 1569 | |||
| 1570 | Ok(dap::ScopesResponse { | ||
| 1571 | scopes: (*frame_2_scopes).clone(), | ||
| 1572 | }) | ||
| 1573 | } | ||
| 1574 | _ => panic!("Made a scopes request with an invalid frame id"), | ||
| 1575 | } | ||
| 1576 | }) | ||
| 1577 | .await; | ||
| 1578 | |||
| 1579 | let frame_1_variables = vec![ | ||
| 1580 | Variable { | ||
| 1581 | name: "variable1".into(), | ||
| 1582 | value: "value 1".into(), | ||
| 1583 | type_: None, | ||
| 1584 | presentation_hint: None, | ||
| 1585 | evaluate_name: None, | ||
| 1586 | variables_reference: 0, | ||
| 1587 | named_variables: None, | ||
| 1588 | indexed_variables: None, | ||
| 1589 | memory_reference: None, | ||
| 1590 | declaration_location_reference: None, | ||
| 1591 | value_location_reference: None, | ||
| 1592 | }, | ||
| 1593 | Variable { | ||
| 1594 | name: "variable2".into(), | ||
| 1595 | value: "value 2".into(), | ||
| 1596 | type_: None, | ||
| 1597 | presentation_hint: None, | ||
| 1598 | evaluate_name: None, | ||
| 1599 | variables_reference: 0, | ||
| 1600 | named_variables: None, | ||
| 1601 | indexed_variables: None, | ||
| 1602 | memory_reference: None, | ||
| 1603 | declaration_location_reference: None, | ||
| 1604 | value_location_reference: None, | ||
| 1605 | }, | ||
| 1606 | ]; | ||
| 1607 | |||
| 1608 | let frame_2_variables = vec![ | ||
| 1609 | Variable { | ||
| 1610 | name: "variable3".into(), | ||
| 1611 | value: "old value 1".into(), | ||
| 1612 | type_: None, | ||
| 1613 | presentation_hint: None, | ||
| 1614 | evaluate_name: None, | ||
| 1615 | variables_reference: 0, | ||
| 1616 | named_variables: None, | ||
| 1617 | indexed_variables: None, | ||
| 1618 | memory_reference: None, | ||
| 1619 | declaration_location_reference: None, | ||
| 1620 | value_location_reference: None, | ||
| 1621 | }, | ||
| 1622 | Variable { | ||
| 1623 | name: "variable4".into(), | ||
| 1624 | value: "old value 2".into(), | ||
| 1625 | type_: None, | ||
| 1626 | presentation_hint: None, | ||
| 1627 | evaluate_name: None, | ||
| 1628 | variables_reference: 0, | ||
| 1629 | named_variables: None, | ||
| 1630 | indexed_variables: None, | ||
| 1631 | memory_reference: None, | ||
| 1632 | declaration_location_reference: None, | ||
| 1633 | value_location_reference: None, | ||
| 1634 | }, | ||
| 1635 | ]; | ||
| 1636 | |||
| 1637 | client | ||
| 1638 | .on_request::<Variables, _>({ | ||
| 1639 | let frame_1_variables = Arc::new(frame_1_variables.clone()); | ||
| 1640 | move |_, args| { | ||
| 1641 | assert_eq!(2, args.variables_reference); | ||
| 1642 | |||
| 1643 | Ok(dap::VariablesResponse { | ||
| 1644 | variables: (*frame_1_variables).clone(), | ||
| 1645 | }) | ||
| 1646 | } | ||
| 1647 | }) | ||
| 1648 | .await; | ||
| 1649 | |||
| 1650 | client | ||
| 1651 | .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { | ||
| 1652 | reason: dap::StoppedEventReason::Pause, | ||
| 1653 | description: None, | ||
| 1654 | thread_id: Some(1), | ||
| 1655 | preserve_focus_hint: None, | ||
| 1656 | text: None, | ||
| 1657 | all_threads_stopped: None, | ||
| 1658 | hit_breakpoint_ids: None, | ||
| 1659 | })) | ||
| 1660 | .await; | ||
| 1661 | |||
| 1662 | cx.run_until_parked(); | ||
| 1663 | |||
| 1664 | let running_state = | ||
| 1665 | active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { | ||
| 1666 | cx.focus_self(window); | ||
| 1667 | item.mode() | ||
| 1668 | .as_running() | ||
| 1669 | .expect("Session should be running by this point") | ||
| 1670 | .clone() | ||
| 1671 | }); | ||
| 1672 | |||
| 1673 | cx.run_until_parked(); | ||
| 1674 | |||
| 1675 | running_state.update(cx, |running_state, cx| { | ||
| 1676 | let (stack_frame_list, stack_frame_id) = | ||
| 1677 | running_state.stack_frame_list().update(cx, |list, _| { | ||
| 1678 | (list.flatten_entries(), list.current_stack_frame_id()) | ||
| 1679 | }); | ||
| 1680 | |||
| 1681 | let variable_list = running_state.variable_list().read(cx); | ||
| 1682 | let variables = variable_list.variables(); | ||
| 1683 | |||
| 1684 | assert_eq!(Some(1), stack_frame_id); | ||
| 1685 | assert_eq!( | ||
| 1686 | running_state | ||
| 1687 | .stack_frame_list() | ||
| 1688 | .read(cx) | ||
| 1689 | .current_stack_frame_id(), | ||
| 1690 | Some(1) | ||
| 1691 | ); | ||
| 1692 | |||
| 1693 | assert!( | ||
| 1694 | called_first_stack_frame.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1695 | "Request scopes shouldn't be called before it's needed" | ||
| 1696 | ); | ||
| 1697 | assert!( | ||
| 1698 | !called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1699 | "Request scopes shouldn't be called before it's needed" | ||
| 1700 | ); | ||
| 1701 | |||
| 1702 | assert_eq!(stack_frames, stack_frame_list); | ||
| 1703 | assert_eq!(frame_1_variables, variables); | ||
| 1704 | }); | ||
| 1705 | |||
| 1706 | client | ||
| 1707 | .on_request::<Variables, _>({ | ||
| 1708 | let frame_2_variables = Arc::new(frame_2_variables.clone()); | ||
| 1709 | move |_, args| { | ||
| 1710 | assert_eq!(3, args.variables_reference); | ||
| 1711 | |||
| 1712 | Ok(dap::VariablesResponse { | ||
| 1713 | variables: (*frame_2_variables).clone(), | ||
| 1714 | }) | ||
| 1715 | } | ||
| 1716 | }) | ||
| 1717 | .await; | ||
| 1718 | |||
| 1719 | running_state | ||
| 1720 | .update_in(cx, |running_state, window, cx| { | ||
| 1721 | running_state | ||
| 1722 | .stack_frame_list() | ||
| 1723 | .update(cx, |stack_frame_list, cx| { | ||
| 1724 | stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) | ||
| 1725 | }) | ||
| 1726 | }) | ||
| 1727 | .await | ||
| 1728 | .unwrap(); | ||
| 1729 | |||
| 1730 | cx.run_until_parked(); | ||
| 1731 | |||
| 1732 | running_state.update(cx, |running_state, cx| { | ||
| 1733 | let (stack_frame_list, stack_frame_id) = | ||
| 1734 | running_state.stack_frame_list().update(cx, |list, _| { | ||
| 1735 | (list.flatten_entries(), list.current_stack_frame_id()) | ||
| 1736 | }); | ||
| 1737 | |||
| 1738 | let variable_list = running_state.variable_list().read(cx); | ||
| 1739 | let variables = variable_list.variables(); | ||
| 1740 | |||
| 1741 | assert_eq!(Some(2), stack_frame_id); | ||
| 1742 | assert!( | ||
| 1743 | called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst), | ||
| 1744 | "Request scopes shouldn't be called before it's needed" | ||
| 1745 | ); | ||
| 1746 | |||
| 1747 | assert_eq!(stack_frames, stack_frame_list); | ||
| 1748 | |||
| 1749 | assert_eq!(variables, frame_2_variables,); | ||
| 1750 | }); | ||
| 1751 | |||
| 1752 | let shutdown_session = project.update(cx, |project, cx| { | ||
| 1753 | project.dap_store().update(cx, |dap_store, cx| { | ||
| 1754 | dap_store.shutdown_session(session.read(cx).session_id(), cx) | ||
| 1755 | }) | ||
| 1756 | }); | ||
| 1757 | |||
| 1758 | shutdown_session.await.unwrap(); | ||
| 1759 | } | ||
| @@ -40,6 +40,7 @@ convert_case.workspace = true · 39 unmodified lines | |||
| 40 | db.workspace = true | 40 | db.workspace = true |
| 41 | buffer_diff.workspace = true | 41 | buffer_diff.workspace = true |
| 42 | emojis.workspace = true | 42 | emojis.workspace = true |
| 43 | feature_flags.workspace = true | ||
| 43 | file_icons.workspace = true | 44 | file_icons.workspace = true |
| 44 | futures.workspace = true | 45 | futures.workspace = true |
| 45 | fuzzy.workspace = true | 46 | fuzzy.workspace = true |
| @@ -55,6 +56,7 @@ linkify.workspace = true · 9 unmodified lines | |||
| 55 | log.workspace = true | 56 | log.workspace = true |
| 56 | lsp.workspace = true | 57 | lsp.workspace = true |
| 57 | markdown.workspace = true | 58 | markdown.workspace = true |
| 59 | menu.workspace = true | ||
| 58 | multi_buffer.workspace = true | 60 | multi_buffer.workspace = true |
| 59 | ordered-float.workspace = true | 61 | ordered-float.workspace = true |
| 60 | parking_lot.workspace = true | 62 | parking_lot.workspace = true |
| @@ -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, |
| @@ -68,6 +68,7 @@ use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, Posi · 67 unmodified lines | |||
| 68 | pub use element::{ | 68 | pub use element::{ |
| 69 | CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, | 69 | CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, |
| 70 | }; | 70 | }; |
| 71 | use feature_flags::{Debugger, FeatureFlagAppExt}; | ||
| 71 | use futures::{ | 72 | use 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 | |||
| 83 | use gpui::{ | 84 | use 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 | }; |
| 93 | use highlight_matching_bracket::refresh_matching_bracket_highlights; | 94 | use highlight_matching_bracket::refresh_matching_bracket_highlights; |
| 95 | use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; | ||
| 94 | use hover_popover::{hide_hover, HoverState}; | 96 | use hover_popover::{hide_hover, HoverState}; |
| 95 | use indent_guides::ActiveIndentGuidesState; | 97 | use indent_guides::ActiveIndentGuidesState; |
| 96 | use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; | 98 | use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; |
| @@ -112,6 +114,11 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} · 15 unmodified lines | |||
| 112 | use linked_editing_ranges::refresh_linked_ranges; | 114 | use linked_editing_ranges::refresh_linked_ranges; |
| 113 | use mouse_context_menu::MouseContextMenu; | 115 | use mouse_context_menu::MouseContextMenu; |
| 114 | use persistence::DB; | 116 | use persistence::DB; |
| 117 | use project::{ | ||
| 118 | debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent}, | ||
| 119 | ProjectPath, | ||
| 120 | }; | ||
| 121 | |||
| 115 | pub use proposed_changes_editor::{ | 122 | pub use proposed_changes_editor::{ |
| 116 | ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, | 123 | ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, |
| 117 | }; | 124 | }; |
| @@ -119,7 +126,6 @@ use smallvec::smallvec; · 1 unmodified line | |||
| 119 | use std::iter::Peekable; | 126 | use std::iter::Peekable; |
| 120 | use task::{ResolvedTask, TaskTemplate, TaskVariables}; | 127 | use task::{ResolvedTask, TaskTemplate, TaskVariables}; |
| 121 | 128 | ||
| 122 | use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; | ||
| 123 | pub use lsp::CompletionContext; | 129 | pub use lsp::CompletionContext; |
| 124 | use lsp::{ | 130 | use 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 | }; |
| 145 | use parking_lot::Mutex; | ||
| 139 | use project::{ | 146 | use 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 | |||
| 153 | use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; | 161 | use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; |
| 154 | use smallvec::SmallVec; | 162 | use smallvec::SmallVec; |
| 155 | use snippet::Snippet; | 163 | use snippet::Snippet; |
| 164 | use std::sync::Arc; | ||
| 156 | use std::{ | 165 | use 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 | }; |
| 169 | pub use sum_tree::Bias; | 177 | pub use sum_tree::Bias; |
| @@ -236,6 +244,7 @@ impl InlayId { · 66 unmodified lines | |||
| 236 | } | 244 | } |
| 237 | } | 245 | } |
| 238 | 246 | ||
| 247 | pub enum DebugCurrentRowHighlight {} | ||
| 239 | enum DocumentHighlightRead {} | 248 | enum DocumentHighlightRead {} |
| 240 | enum DocumentHighlightWrite {} | 249 | enum DocumentHighlightWrite {} |
| 241 | enum InputComposition {} | 250 | enum 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)] |
| 593 | struct BufferOffset(usize); | 603 | struct 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 | None | 1331 | 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_length | 18253 | 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.0 | 18273 | 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.0 | 18275 | 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.0 | 18277 | em_width * 2.0 |
| @@ -18668,6 +19271,157 @@ impl Global for KillRing {} · 993 unmodified lines | |||
| 18668 | 19271 | ||
| 18669 | const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); | 19272 | const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); |
| 18670 | 19273 | ||
| 19274 | struct BreakpointPromptEditor { | ||
| 19275 | pub(crate) prompt: Entity<Editor>, | ||
| 19276 | editor: WeakEntity<Editor>, | ||
| 19277 | breakpoint_anchor: Anchor, | ||
| 19278 | kind: BreakpointKind, | ||
| 19279 | block_ids: HashSet<CustomBlockId>, | ||
| 19280 | gutter_dimensions: Arc<Mutex<GutterDimensions>>, | ||
| 19281 | _subscriptions: Vec<Subscription>, | ||
| 19282 | } | ||
| 19283 | |||
| 19284 | impl BreakpointPromptEditor { | ||
| 19285 | const MAX_LINES: u8 = 4; | ||
| 19286 | |||
| 19287 | fn new( | ||
| 19288 | editor: WeakEntity<Editor>, | ||
| 19289 | breakpoint_anchor: Anchor, | ||
| 19290 | kind: BreakpointKind, | ||
| 19291 | window: &mut Window, | ||
| 19292 | cx: &mut Context<Self>, | ||
| 19293 | ) -> Self { | ||
| 19294 | let buffer = cx.new(|cx| { | ||
| 19295 | Buffer::local( | ||
| 19296 | kind.log_message() | ||
| 19297 | .map(|msg| msg.to_string()) | ||
| 19298 | .unwrap_or_default(), | ||
| 19299 | cx, | ||
| 19300 | ) | ||
| 19301 | }); | ||
| 19302 | let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); | ||
| 19303 | |||
| 19304 | let prompt = cx.new(|cx| { | ||
| 19305 | let mut prompt = Editor::new( | ||
| 19306 | EditorMode::AutoHeight { | ||
| 19307 | max_lines: Self::MAX_LINES as usize, | ||
| 19308 | }, | ||
| 19309 | buffer, | ||
| 19310 | None, | ||
| 19311 | window, | ||
| 19312 | cx, | ||
| 19313 | ); | ||
| 19314 | prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); | ||
| 19315 | prompt.set_show_cursor_when_unfocused(false, cx); | ||
| 19316 | prompt.set_placeholder_text( | ||
| 19317 | "Message to log when breakpoint is hit. Expressions within {} are interpolated.", | ||
| 19318 | cx, | ||
| 19319 | ); | ||
| 19320 | |||
| 19321 | prompt | ||
| 19322 | }); | ||
| 19323 | |||
| 19324 | Self { | ||
| 19325 | prompt, | ||
| 19326 | editor, | ||
| 19327 | breakpoint_anchor, | ||
| 19328 | kind, | ||
| 19329 | gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), | ||
| 19330 | block_ids: Default::default(), | ||
| 19331 | _subscriptions: vec![], | ||
| 19332 | } | ||
| 19333 | } | ||
| 19334 | |||
| 19335 | pub(crate) fn add_block_ids(&mut self, block_ids: Vec<CustomBlockId>) { | ||
| 19336 | self.block_ids.extend(block_ids) | ||
| 19337 | } | ||
| 19338 | |||
| 19339 | fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { | ||
| 19340 | if let Some(editor) = self.editor.upgrade() { | ||
| 19341 | let log_message = self | ||
| 19342 | .prompt | ||
| 19343 | .read(cx) | ||
| 19344 | .buffer | ||
| 19345 | .read(cx) | ||
| 19346 | .as_singleton() | ||
| 19347 | .expect("A multi buffer in breakpoint prompt isn't possible") | ||
| 19348 | .read(cx) | ||
| 19349 | .as_rope() | ||
| 19350 | .to_string(); | ||
| 19351 | |||
| 19352 | editor.update(cx, |editor, cx| { | ||
| 19353 | editor.edit_breakpoint_at_anchor( | ||
| 19354 | self.breakpoint_anchor, | ||
| 19355 | self.kind.clone(), | ||
| 19356 | BreakpointEditAction::EditLogMessage(log_message.into()), | ||
| 19357 | cx, | ||
| 19358 | ); | ||
| 19359 | |||
| 19360 | editor.remove_blocks(self.block_ids.clone(), None, cx); | ||
| 19361 | cx.focus_self(window); | ||
| 19362 | }); | ||
| 19363 | } | ||
| 19364 | } | ||
| 19365 | |||
| 19366 | fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) { | ||
| 19367 | self.editor | ||
| 19368 | .update(cx, |editor, cx| { | ||
| 19369 | editor.remove_blocks(self.block_ids.clone(), None, cx); | ||
| 19370 | window.focus(&editor.focus_handle); | ||
| 19371 | }) | ||
| 19372 | .log_err(); | ||
| 19373 | } | ||
| 19374 | |||
| 19375 | fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 19376 | let settings = ThemeSettings::get_global(cx); | ||
| 19377 | let text_style = TextStyle { | ||
| 19378 | color: if self.prompt.read(cx).read_only(cx) { | ||
| 19379 | cx.theme().colors().text_disabled | ||
| 19380 | } else { | ||
| 19381 | cx.theme().colors().text | ||
| 19382 | }, | ||
| 19383 | font_family: settings.buffer_font.family.clone(), | ||
| 19384 | font_fallbacks: settings.buffer_font.fallbacks.clone(), | ||
| 19385 | font_size: settings.buffer_font_size(cx).into(), | ||
| 19386 | font_weight: settings.buffer_font.weight, | ||
| 19387 | line_height: relative(settings.buffer_line_height.value()), | ||
| 19388 | ..Default::default() | ||
| 19389 | }; | ||
| 19390 | EditorElement::new( | ||
| 19391 | &self.prompt, | ||
| 19392 | EditorStyle { | ||
| 19393 | background: cx.theme().colors().editor_background, | ||
| 19394 | local_player: cx.theme().players().local(), | ||
| 19395 | text: text_style, | ||
| 19396 | ..Default::default() | ||
| 19397 | }, | ||
| 19398 | ) | ||
| 19399 | } | ||
| 19400 | } | ||
| 19401 | |||
| 19402 | impl Render for BreakpointPromptEditor { | ||
| 19403 | fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | ||
| 19404 | let gutter_dimensions = *self.gutter_dimensions.lock(); | ||
| 19405 | h_flex() | ||
| 19406 | .key_context("Editor") | ||
| 19407 | .bg(cx.theme().colors().editor_background) | ||
| 19408 | .border_y_1() | ||
| 19409 | .border_color(cx.theme().status().info_border) | ||
| 19410 | .size_full() | ||
| 19411 | .py(window.line_height() / 2.5) | ||
| 19412 | .on_action(cx.listener(Self::confirm)) | ||
| 19413 | .on_action(cx.listener(Self::cancel)) | ||
| 19414 | .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) | ||
| 19415 | .child(div().flex_1().child(self.render_prompt_editor(cx))) | ||
| 19416 | } | ||
| 19417 | } | ||
| 19418 | |||
| 19419 | impl Focusable for BreakpointPromptEditor { | ||
| 19420 | fn focus_handle(&self, cx: &App) -> FocusHandle { | ||
| 19421 | self.prompt.focus_handle(cx) | ||
| 19422 | } | ||
| 19423 | } | ||
| 19424 | |||
| 18671 | fn all_edits_insertions_or_deletions( | 19425 | fn all_edits_insertions_or_deletions( |
| 18672 | edits: &Vec<(Range<Anchor>, String)>, | 19426 | edits: &Vec<(Range<Anchor>, String)>, |
| 18673 | snapshot: &MultiBufferSnapshot, | 19427 | snapshot: &MultiBufferSnapshot, |
| @@ -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: true | 466 | /// 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: true | 474 | /// Default: true |
| @@ -28,8 +28,11 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings}; · 27 unmodified lines | |||
| 28 | use multi_buffer::{IndentGuide, PathKey}; | 28 | use multi_buffer::{IndentGuide, PathKey}; |
| 29 | use parking_lot::Mutex; | 29 | use parking_lot::Mutex; |
| 30 | use pretty_assertions::{assert_eq, assert_ne}; | 30 | use pretty_assertions::{assert_eq, assert_ne}; |
| 31 | use project::project_settings::{LspSettings, ProjectSettings}; | 31 | use project::{ |
| 32 | use project::FakeFs; | 32 | debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}, |
| 33 | project_settings::{LspSettings, ProjectSettings}, | ||
| 34 | FakeFs, | ||
| 35 | }; | ||
| 33 | use serde_json::{self, json}; | 36 | use serde_json::{self, json}; |
| 34 | use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; | 37 | use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; |
| 35 | use std::{ | 38 | use 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] | ||
| 17119 | fn assert_breakpoint( | ||
| 17120 | breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>, | ||
| 17121 | path: &Arc<Path>, | ||
| 17122 | expected: Vec<(u32, BreakpointKind)>, | ||
| 17123 | ) { | ||
| 17124 | if expected.len() == 0usize { | ||
| 17125 | assert!(!breakpoints.contains_key(path)); | ||
| 17126 | } else { | ||
| 17127 | let mut breakpoint = breakpoints | ||
| 17128 | .get(path) | ||
| 17129 | .unwrap() | ||
| 17130 | .into_iter() | ||
| 17131 | .map(|breakpoint| (breakpoint.position, breakpoint.kind.clone())) | ||
| 17132 | .collect::<Vec<_>>(); | ||
| 17133 | |||
| 17134 | breakpoint.sort_by_key(|(cached_position, _)| *cached_position); | ||
| 17135 | |||
| 17136 | assert_eq!(expected, breakpoint); | ||
| 17137 | } | ||
| 17138 | } | ||
| 17139 | |||
| 17140 | fn add_log_breakpoint_at_cursor( | ||
| 17141 | editor: &mut Editor, | ||
| 17142 | log_message: &str, | ||
| 17143 | window: &mut Window, | ||
| 17144 | cx: &mut Context<Editor>, | ||
| 17145 | ) { | ||
| 17146 | let (anchor, bp) = editor | ||
| 17147 | .breakpoint_at_cursor_head(window, cx) | ||
| 17148 | .unwrap_or_else(|| { | ||
| 17149 | let cursor_position: Point = editor.selections.newest(cx).head(); | ||
| 17150 | |||
| 17151 | let breakpoint_position = editor | ||
| 17152 | .snapshot(window, cx) | ||
| 17153 | .display_snapshot | ||
| 17154 | .buffer_snapshot | ||
| 17155 | .anchor_before(Point::new(cursor_position.row, 0)); | ||
| 17156 | |||
| 17157 | let kind = BreakpointKind::Log(Arc::from(log_message)); | ||
| 17158 | |||
| 17159 | (breakpoint_position, Breakpoint { kind }) | ||
| 17160 | }); | ||
| 17161 | |||
| 17162 | editor.edit_breakpoint_at_anchor( | ||
| 17163 | anchor, | ||
| 17164 | bp.kind, | ||
| 17165 | BreakpointEditAction::EditLogMessage(log_message.into()), | ||
| 17166 | cx, | ||
| 17167 | ); | ||
| 17168 | } | ||
| 17169 | |||
| 17170 | #[gpui::test] | ||
| 17171 | async fn test_breakpoint_toggling(cx: &mut TestAppContext) { | ||
| 17172 | init_test(cx, |_| {}); | ||
| 17173 | |||
| 17174 | let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string(); | ||
| 17175 | let fs = FakeFs::new(cx.executor()); | ||
| 17176 | fs.insert_tree( | ||
| 17177 | path!("/a"), | ||
| 17178 | json!({ | ||
| 17179 | "main.rs": sample_text, | ||
| 17180 | }), | ||
| 17181 | ) | ||
| 17182 | .await; | ||
| 17183 | let project = Project::test(fs, [path!("/a").as_ref()], cx).await; | ||
| 17184 | let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); | ||
| 17185 | let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); | ||
| 17186 | |||
| 17187 | let fs = FakeFs::new(cx.executor()); | ||
| 17188 | fs.insert_tree( | ||
| 17189 | path!("/a"), | ||
| 17190 | json!({ | ||
| 17191 | "main.rs": sample_text, | ||
| 17192 | }), | ||
| 17193 | ) | ||
| 17194 | .await; | ||
| 17195 | let project = Project::test(fs, [path!("/a").as_ref()], cx).await; | ||
| 17196 | let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); | ||
| 17197 | let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); | ||
| 17198 | let worktree_id = workspace | ||
| 17199 | .update(cx, |workspace, _window, cx| { | ||
| 17200 | workspace.project().update(cx, |project, cx| { | ||
| 17201 | project.worktrees(cx).next().unwrap().read(cx).id() | ||
| 17202 | }) | ||
| 17203 | }) | ||
| 17204 | .unwrap(); | ||
| 17205 | |||
| 17206 | let buffer = project | ||
| 17207 | .update(cx, |project, cx| { | ||
| 17208 | project.open_buffer((worktree_id, "main.rs"), cx) | ||
| 17209 | }) | ||
| 17210 | .await | ||
| 17211 | .unwrap(); | ||
| 17212 | |||
| 17213 | let (editor, cx) = cx.add_window_view(|window, cx| { | ||
| 17214 | Editor::new( | ||
| 17215 | EditorMode::Full, | ||
| 17216 | MultiBuffer::build_from_buffer(buffer, cx), | ||
| 17217 | Some(project.clone()), | ||
| 17218 | window, | ||
| 17219 | cx, | ||
| 17220 | ) | ||
| 17221 | }); | ||
| 17222 | |||
| 17223 | let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap()); | ||
| 17224 | let abs_path = project.read_with(cx, |project, cx| { | ||
| 17225 | project | ||
| 17226 | .absolute_path(&project_path, cx) | ||
| 17227 | .map(|path_buf| Arc::from(path_buf.to_owned())) | ||
| 17228 | .unwrap() | ||
| 17229 | }); | ||
| 17230 | |||
| 17231 | // assert we can add breakpoint on the first line | ||
| 17232 | editor.update_in(cx, |editor, window, cx| { | ||
| 17233 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17234 | editor.move_to_end(&MoveToEnd, window, cx); | ||
| 17235 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17236 | }); | ||
| 17237 | |||
| 17238 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17239 | editor | ||
| 17240 | .breakpoint_store() | ||
| 17241 | .as_ref() | ||
| 17242 | .unwrap() | ||
| 17243 | .read(cx) | ||
| 17244 | .all_breakpoints(cx) | ||
| 17245 | .clone() | ||
| 17246 | }); | ||
| 17247 | |||
| 17248 | assert_eq!(1, breakpoints.len()); | ||
| 17249 | assert_breakpoint( | ||
| 17250 | &breakpoints, | ||
| 17251 | &abs_path, | ||
| 17252 | vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], | ||
| 17253 | ); | ||
| 17254 | |||
| 17255 | editor.update_in(cx, |editor, window, cx| { | ||
| 17256 | editor.move_to_beginning(&MoveToBeginning, window, cx); | ||
| 17257 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17258 | }); | ||
| 17259 | |||
| 17260 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17261 | editor | ||
| 17262 | .breakpoint_store() | ||
| 17263 | .as_ref() | ||
| 17264 | .unwrap() | ||
| 17265 | .read(cx) | ||
| 17266 | .all_breakpoints(cx) | ||
| 17267 | .clone() | ||
| 17268 | }); | ||
| 17269 | |||
| 17270 | assert_eq!(1, breakpoints.len()); | ||
| 17271 | assert_breakpoint(&breakpoints, &abs_path, vec![(3, BreakpointKind::Standard)]); | ||
| 17272 | |||
| 17273 | editor.update_in(cx, |editor, window, cx| { | ||
| 17274 | editor.move_to_end(&MoveToEnd, window, cx); | ||
| 17275 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17276 | }); | ||
| 17277 | |||
| 17278 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17279 | editor | ||
| 17280 | .breakpoint_store() | ||
| 17281 | .as_ref() | ||
| 17282 | .unwrap() | ||
| 17283 | .read(cx) | ||
| 17284 | .all_breakpoints(cx) | ||
| 17285 | .clone() | ||
| 17286 | }); | ||
| 17287 | |||
| 17288 | assert_eq!(0, breakpoints.len()); | ||
| 17289 | assert_breakpoint(&breakpoints, &abs_path, vec![]); | ||
| 17290 | } | ||
| 17291 | |||
| 17292 | #[gpui::test] | ||
| 17293 | async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { | ||
| 17294 | init_test(cx, |_| {}); | ||
| 17295 | |||
| 17296 | let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string(); | ||
| 17297 | |||
| 17298 | let fs = FakeFs::new(cx.executor()); | ||
| 17299 | fs.insert_tree( | ||
| 17300 | path!("/a"), | ||
| 17301 | json!({ | ||
| 17302 | "main.rs": sample_text, | ||
| 17303 | }), | ||
| 17304 | ) | ||
| 17305 | .await; | ||
| 17306 | let project = Project::test(fs, [path!("/a").as_ref()], cx).await; | ||
| 17307 | let (workspace, cx) = | ||
| 17308 | cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); | ||
| 17309 | |||
| 17310 | let worktree_id = workspace.update(cx, |workspace, cx| { | ||
| 17311 | workspace.project().update(cx, |project, cx| { | ||
| 17312 | project.worktrees(cx).next().unwrap().read(cx).id() | ||
| 17313 | }) | ||
| 17314 | }); | ||
| 17315 | |||
| 17316 | let buffer = project | ||
| 17317 | .update(cx, |project, cx| { | ||
| 17318 | project.open_buffer((worktree_id, "main.rs"), cx) | ||
| 17319 | }) | ||
| 17320 | .await | ||
| 17321 | .unwrap(); | ||
| 17322 | |||
| 17323 | let (editor, cx) = cx.add_window_view(|window, cx| { | ||
| 17324 | Editor::new( | ||
| 17325 | EditorMode::Full, | ||
| 17326 | MultiBuffer::build_from_buffer(buffer, cx), | ||
| 17327 | Some(project.clone()), | ||
| 17328 | window, | ||
| 17329 | cx, | ||
| 17330 | ) | ||
| 17331 | }); | ||
| 17332 | |||
| 17333 | let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap()); | ||
| 17334 | let abs_path = project.read_with(cx, |project, cx| { | ||
| 17335 | project | ||
| 17336 | .absolute_path(&project_path, cx) | ||
| 17337 | .map(|path_buf| Arc::from(path_buf.to_owned())) | ||
| 17338 | .unwrap() | ||
| 17339 | }); | ||
| 17340 | |||
| 17341 | editor.update_in(cx, |editor, window, cx| { | ||
| 17342 | add_log_breakpoint_at_cursor(editor, "hello world", window, cx); | ||
| 17343 | }); | ||
| 17344 | |||
| 17345 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17346 | editor | ||
| 17347 | .breakpoint_store() | ||
| 17348 | .as_ref() | ||
| 17349 | .unwrap() | ||
| 17350 | .read(cx) | ||
| 17351 | .all_breakpoints(cx) | ||
| 17352 | .clone() | ||
| 17353 | }); | ||
| 17354 | |||
| 17355 | assert_breakpoint( | ||
| 17356 | &breakpoints, | ||
| 17357 | &abs_path, | ||
| 17358 | vec![(0, BreakpointKind::Log("hello world".into()))], | ||
| 17359 | ); | ||
| 17360 | |||
| 17361 | // Removing a log message from a log breakpoint should remove it | ||
| 17362 | editor.update_in(cx, |editor, window, cx| { | ||
| 17363 | add_log_breakpoint_at_cursor(editor, "", window, cx); | ||
| 17364 | }); | ||
| 17365 | |||
| 17366 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17367 | editor | ||
| 17368 | .breakpoint_store() | ||
| 17369 | .as_ref() | ||
| 17370 | .unwrap() | ||
| 17371 | .read(cx) | ||
| 17372 | .all_breakpoints(cx) | ||
| 17373 | .clone() | ||
| 17374 | }); | ||
| 17375 | |||
| 17376 | assert_breakpoint(&breakpoints, &abs_path, vec![]); | ||
| 17377 | |||
| 17378 | editor.update_in(cx, |editor, window, cx| { | ||
| 17379 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17380 | editor.move_to_end(&MoveToEnd, window, cx); | ||
| 17381 | editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); | ||
| 17382 | // Not adding a log message to a standard breakpoint shouldn't remove it | ||
| 17383 | add_log_breakpoint_at_cursor(editor, "", window, cx); | ||
| 17384 | }); | ||
| 17385 | |||
| 17386 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17387 | editor | ||
| 17388 | .breakpoint_store() | ||
| 17389 | .as_ref() | ||
| 17390 | .unwrap() | ||
| 17391 | .read(cx) | ||
| 17392 | .all_breakpoints(cx) | ||
| 17393 | .clone() | ||
| 17394 | }); | ||
| 17395 | |||
| 17396 | assert_breakpoint( | ||
| 17397 | &breakpoints, | ||
| 17398 | &abs_path, | ||
| 17399 | vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], | ||
| 17400 | ); | ||
| 17401 | |||
| 17402 | editor.update_in(cx, |editor, window, cx| { | ||
| 17403 | add_log_breakpoint_at_cursor(editor, "hello world", window, cx); | ||
| 17404 | }); | ||
| 17405 | |||
| 17406 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17407 | editor | ||
| 17408 | .breakpoint_store() | ||
| 17409 | .as_ref() | ||
| 17410 | .unwrap() | ||
| 17411 | .read(cx) | ||
| 17412 | .all_breakpoints(cx) | ||
| 17413 | .clone() | ||
| 17414 | }); | ||
| 17415 | |||
| 17416 | assert_breakpoint( | ||
| 17417 | &breakpoints, | ||
| 17418 | &abs_path, | ||
| 17419 | vec![ | ||
| 17420 | (0, BreakpointKind::Standard), | ||
| 17421 | (3, BreakpointKind::Log("hello world".into())), | ||
| 17422 | ], | ||
| 17423 | ); | ||
| 17424 | |||
| 17425 | editor.update_in(cx, |editor, window, cx| { | ||
| 17426 | add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx); | ||
| 17427 | }); | ||
| 17428 | |||
| 17429 | let breakpoints = editor.update(cx, |editor, cx| { | ||
| 17430 | editor | ||
| 17431 | .breakpoint_store() | ||
| 17432 | .as_ref() | ||
| 17433 | .unwrap() | ||
| 17434 | .read(cx) | ||
| 17435 | .all_breakpoints(cx) | ||
| 17436 | .clone() | ||
| 17437 | }); | ||
| 17438 | |||
| 17439 | assert_breakpoint( | ||
| 17440 | &breakpoints, | ||
| 17441 | &abs_path, | ||
| 17442 | vec![ | ||
| 17443 | (0, BreakpointKind::Standard), | ||
| 17444 | (3, BreakpointKind::Log("hello Earth !!".into())), | ||
| 17445 | ], | ||
| 17446 | ); | ||
| 17447 | } | ||
| 17448 | |||
| 17114 | #[gpui::test] | 17449 | #[gpui::test] |
| 17115 | async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) { | 17450 | async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) { |
| 17116 | init_test(cx, |_| {}); | 17451 | init_test(cx, |_| {}); |
| @@ -29,6 +29,7 @@ use crate::{ · 28 unmodified lines | |||
| 29 | use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; | 29 | use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; |
| 30 | use client::ParticipantIndex; | 30 | use client::ParticipantIndex; |
| 31 | use collections::{BTreeMap, HashMap, HashSet}; | 31 | use collections::{BTreeMap, HashMap, HashSet}; |
| 32 | use feature_flags::{Debugger, FeatureFlagAppExt}; | ||
| 32 | use file_icons::FileIcons; | 33 | use file_icons::FileIcons; |
| 33 | use git::{blame::BlameEntry, status::FileStatus, Oid}; | 34 | use git::{blame::BlameEntry, status::FileStatus, Oid}; |
| 34 | use gpui::{ | 35 | use 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 | }; |
| 58 | use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings}; | 59 | use project::{ |
| 60 | debugger::breakpoint_store::{Breakpoint, BreakpointKind}, | ||
| 61 | project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, | ||
| 62 | }; | ||
| 59 | use settings::Settings; | 63 | use settings::Settings; |
| 60 | use smallvec::{smallvec, SmallVec}; | 64 | use smallvec::{smallvec, SmallVec}; |
| 61 | use std::{ | 65 | use std::{ |
| @@ -82,6 +86,14 @@ use workspace::{item::Item, notifications::NotifyTaskExt}; · 20 unmodified lines | |||
| 82 | const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; | 86 | const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; |
| 83 | const MIN_SCROLL_THUMB_SIZE: f32 = 25.; | 87 | const 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)] | ||
| 91 | struct LineHighlightSpec { | ||
| 92 | selection: bool, | ||
| 93 | breakpoint: bool, | ||
| 94 | _active_stack_frame: bool, | ||
| 95 | } | ||
| 96 | |||
| 85 | struct SelectionLayout { | 97 | struct 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 menu | 906 | // 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_infos | 2171 | 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 = self | 2467 | 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 + 1 | 4134 | active_row.0 == end_row + 1 |
| 4038 | && *has_selection == contains_non_empty_selection | 4135 | && 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_number | 4395 | .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_number | 4397 | else { |
| 4301 | } else { | 4398 | continue; |
| 4302 | cx.theme().colors().editor_line_number | 4399 | }; |
| 4303 | }; | ||
| 4304 | 4400 | ||
| 4305 | let Some(line) = self | 4401 | 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 | |||
| 7725 | struct LineNumberLayout { | 7883 | struct 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 | ||
| 7731 | struct ColoredRange<T> { | 7888 | struct 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, |
| @@ -4,7 +4,7 @@ use std::sync::Arc; · 3 unmodified lines | |||
| 4 | use anyhow::Result; | 4 | use anyhow::Result; |
| 5 | use fs::Fs; | 5 | use fs::Fs; |
| 6 | use gpui::{App, Global, ReadGlobal, SharedString, Task}; | 6 | use gpui::{App, Global, ReadGlobal, SharedString, Task}; |
| 7 | use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage}; | 7 | use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage}; |
| 8 | use lsp::LanguageServerName; | 8 | use lsp::LanguageServerName; |
| 9 | use parking_lot::RwLock; | 9 | use 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; |
| @@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines | |||
| 8 | use extension::ExtensionHostProxy; | 8 | use extension::ExtensionHostProxy; |
| 9 | use fs::{FakeFs, Fs, RealFs}; | 9 | use fs::{FakeFs, Fs, RealFs}; |
| 10 | use futures::{io::BufReader, AsyncReadExt, StreamExt}; | 10 | use futures::{io::BufReader, AsyncReadExt, StreamExt}; |
| 11 | use gpui::{AppContext as _, SemanticVersion, TestAppContext}; | 11 | use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext}; |
| 12 | use http_client::{FakeHttpClient, Response}; | 12 | use http_client::{FakeHttpClient, Response}; |
| 13 | use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus}; | 13 | use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; |
| 14 | use lsp::LanguageServerName; | 14 | use lsp::LanguageServerName; |
| 15 | use node_runtime::NodeRuntime; | 15 | use node_runtime::NodeRuntime; |
| 16 | use parking_lot::Mutex; | 16 | use 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::CheckingForUpdate | 668 | (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 | ||
| @@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4; · 2 unmodified lines | |||
| 3 | use crate::wasm_host::WasmState; | 3 | use crate::wasm_host::WasmState; |
| 4 | use anyhow::Result; | 4 | use anyhow::Result; |
| 5 | use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; | 5 | use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; |
| 6 | use language::LanguageServerBinaryStatus; | 6 | use language::BinaryStatus; |
| 7 | use semantic_version::SemanticVersion; | 7 | use semantic_version::SemanticVersion; |
| 8 | use std::sync::{Arc, OnceLock}; | 8 | use std::sync::{Arc, OnceLock}; |
| 9 | use wasmtime::component::{Linker, Resource}; | 9 | use 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::CheckingForUpdate | 136 | LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, |
| 137 | } | ||
| 138 | LanguageServerInstallationStatus::Downloading => { | ||
| 139 | LanguageServerBinaryStatus::Downloading | ||
| 140 | } | ||
| 141 | LanguageServerInstallationStatus::Cached | 137 | 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.host | 142 | self.host |
| @@ -8,7 +8,7 @@ use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDel · 7 unmodified lines | |||
| 8 | use futures::{io::BufReader, FutureExt as _}; | 8 | use futures::{io::BufReader, FutureExt as _}; |
| 9 | use futures::{lock::Mutex, AsyncReadExt}; | 9 | use futures::{lock::Mutex, AsyncReadExt}; |
| 10 | use language::LanguageName; | 10 | use language::LanguageName; |
| 11 | use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus}; | 11 | use language::{language_settings::AllLanguageSettings, BinaryStatus}; |
| 12 | use project::project_settings::ProjectSettings; | 12 | use project::project_settings::ProjectSettings; |
| 13 | use semantic_version::SemanticVersion; | 13 | use semantic_version::SemanticVersion; |
| 14 | use std::{ | 14 | use 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::CheckingForUpdate | 478 | 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.host | 483 | self.host |
| @@ -13,7 +13,7 @@ use extension::{ · 12 unmodified lines | |||
| 13 | }; | 13 | }; |
| 14 | use futures::{io::BufReader, FutureExt as _}; | 14 | use futures::{io::BufReader, FutureExt as _}; |
| 15 | use futures::{lock::Mutex, AsyncReadExt}; | 15 | use futures::{lock::Mutex, AsyncReadExt}; |
| 16 | use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus}; | 16 | use language::{language_settings::AllLanguageSettings, BinaryStatus, LanguageName}; |
| 17 | use project::project_settings::ProjectSettings; | 17 | use project::project_settings::ProjectSettings; |
| 18 | use semantic_version::SemanticVersion; | 18 | use semantic_version::SemanticVersion; |
| 19 | use std::{ | 19 | use 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::CheckingForUpdate | 696 | 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.host | 701 | self.host |
| @@ -115,6 +115,11 @@ impl FeatureFlag for AutoCommand { · 114 unmodified lines | |||
| 115 | } | 115 | } |
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | pub struct Debugger {} | ||
| 119 | impl FeatureFlag for Debugger { | ||
| 120 | const NAME: &'static str = "debugger"; | ||
| 121 | } | ||
| 122 | |||
| 118 | pub trait FeatureFlagViewExt<V: 'static> { | 123 | pub trait FeatureFlagViewExt<V: 'static> { |
| 119 | fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription | 124 | fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription |
| 120 | where | 125 | 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 { |
| @@ -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 the | 2542 | // 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!( |
| @@ -73,8 +73,8 @@ pub use buffer::Operation; · 72 unmodified lines | |||
| 73 | pub use buffer::*; | 73 | pub use buffer::*; |
| 74 | pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup}; | 74 | pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup}; |
| 75 | pub use language_registry::{ | 75 | pub 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 | }; |
| 79 | pub use lsp::{LanguageServerId, LanguageServerName}; | 79 | pub use lsp::{LanguageServerId, LanguageServerName}; |
| 80 | pub use outline::*; | 80 | pub 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 = adapter | 591 | 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 | .await | 597 | .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 = adapter | 605 | 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 | binary | 610 | binary |
| 611 | } | 611 | } |
| 612 | } | 612 | } |
| @@ -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 | ||
| 104 | struct LanguageRegistryState { | 105 | struct 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)] |
| 133 | pub enum LanguageServerBinaryStatus { | 134 | pub 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)] |
| 216 | struct LspBinaryStatusSender { | 217 | struct BinaryStatusSender { |
| 217 | txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>, | 218 | txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(SharedString, BinaryStatus)>>>>, |
| 218 | } | 219 | } |
| 219 | 220 | ||
| 220 | pub struct LoadedLanguage { | 221 | pub 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 | ||
| 1091 | impl LspBinaryStatusSender { | 1099 | impl 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 | rx | 1103 | 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 | } |
| @@ -12,8 +12,8 @@ use fs::Fs; · 11 unmodified lines | |||
| 12 | use futures::{Future, FutureExt}; | 12 | use futures::{Future, FutureExt}; |
| 13 | use gpui::AsyncApp; | 13 | use gpui::AsyncApp; |
| 14 | use language::{ | 14 | use language::{ |
| 15 | CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus, | 15 | BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, |
| 16 | LanguageToolchainStore, LspAdapter, LspAdapterDelegate, | 16 | LspAdapter, LspAdapterDelegate, |
| 17 | }; | 17 | }; |
| 18 | use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; | 18 | use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; |
| 19 | use serde::Serialize; | 19 | use 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_registry | 85 | self.language_registry |
| 86 | .update_lsp_status(language_server_id, status); | 86 | .update_lsp_status(language_server_id, status); |
| @@ -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 | }) |
| @@ -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. | ||
| 173 | pub fn debug_tasks_file() -> &'static PathBuf { | ||
| 174 | static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new(); | ||
| 175 | DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json")) | ||
| 176 | } | ||
| 177 | |||
| 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. | ||
| 296 | pub fn debug_adapters_dir() -> &'static PathBuf { | ||
| 297 | static DEBUG_ADAPTERS_DIR: OnceLock<PathBuf> = OnceLock::new(); | ||
| 298 | DEBUG_ADAPTERS_DIR.get_or_init(|| support_dir().join("debug_adapters")) | ||
| 299 | } | ||
| 300 | |||
| 287 | /// Returns the path to the Copilot directory. | 301 | /// Returns the path to the Copilot directory. |
| 288 | pub fn copilot_dir() -> &'static PathBuf { | 302 | pub 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. | ||
| 346 | pub fn local_debug_file_relative_path() -> &'static Path { | ||
| 347 | Path::new(".zed/debug.json") | ||
| 348 | } | ||
| 349 | |||
| 350 | /// Returns the relative path to a `.vscode/launch.json` file within a project. | ||
| 351 | pub fn local_vscode_launch_file_relative_path() -> &'static Path { | ||
| 352 | Path::new(".vscode/launch.json") | ||
| 353 | } | ||
| 354 | |||
| 331 | /// A default editorconfig file name to use when resolving project settings. | 355 | /// A default editorconfig file name to use when resolving project settings. |
| 332 | pub const EDITORCONFIG_NAME: &str = ".editorconfig"; | 356 | pub const EDITORCONFIG_NAME: &str = ".editorconfig"; |
| @@ -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 | |||
| 30 | askpass.workspace = true | 32 | askpass.workspace = true |
| 31 | async-trait.workspace = true | 33 | async-trait.workspace = true |
| 32 | buffer_diff.workspace = true | 34 | buffer_diff.workspace = true |
| 35 | circular-buffer.workspace = true | ||
| 33 | client.workspace = true | 36 | client.workspace = true |
| 34 | clock.workspace = true | 37 | clock.workspace = true |
| 35 | collections.workspace = true | 38 | collections.workspace = true |
| 39 | dap.workspace = true | ||
| 40 | dap_adapters.workspace = true | ||
| 36 | extension.workspace = true | 41 | extension.workspace = true |
| 37 | fancy-regex.workspace = true | 42 | fancy-regex.workspace = true |
| 38 | fs.workspace = true | 43 | fs.workspace = true |
| @@ -44,6 +49,7 @@ gpui.workspace = true · 5 unmodified lines | |||
| 44 | http_client.workspace = true | 49 | http_client.workspace = true |
| 45 | image.workspace = true | 50 | image.workspace = true |
| 46 | itertools.workspace = true | 51 | itertools.workspace = true |
| 52 | indexmap.workspace = true | ||
| 47 | language.workspace = true | 53 | language.workspace = true |
| 48 | log.workspace = true | 54 | log.workspace = true |
| 49 | lsp.workspace = true | 55 | lsp.workspace = true |
| @@ -81,17 +87,19 @@ worktree.workspace = true · 31 unmodified lines | |||
| 81 | client = { workspace = true, features = ["test-support"] } | 87 | client = { workspace = true, features = ["test-support"] } |
| 82 | collections = { workspace = true, features = ["test-support"] } | 88 | collections = { workspace = true, features = ["test-support"] } |
| 83 | buffer_diff = { workspace = true, features = ["test-support"] } | 89 | buffer_diff = { workspace = true, features = ["test-support"] } |
| 90 | dap = { workspace = true, features = ["test-support"] } | ||
| 91 | dap_adapters = { workspace = true, features = ["test-support"] } | ||
| 84 | env_logger.workspace = true | 92 | env_logger.workspace = true |
| 85 | fs = { workspace = true, features = ["test-support"] } | 93 | fs = { workspace = true, features = ["test-support"] } |
| 86 | git2.workspace = true | 94 | git2.workspace = true |
| 87 | gpui = { workspace = true, features = ["test-support"] } | 95 | gpui = { workspace = true, features = ["test-support"] } |
| 88 | language = { workspace = true, features = ["test-support"] } | 96 | language = { workspace = true, features = ["test-support"] } |
| 89 | release_channel.workspace = true | ||
| 90 | lsp = { workspace = true, features = ["test-support"] } | 97 | lsp = { workspace = true, features = ["test-support"] } |
| 91 | prettier = { workspace = true, features = ["test-support"] } | 98 | prettier = { workspace = true, features = ["test-support"] } |
| 92 | pretty_assertions.workspace = true | 99 | pretty_assertions.workspace = true |
| 93 | worktree = { workspace = true, features = ["test-support"] } | 100 | release_channel.workspace = true |
| 94 | rpc = { workspace = true, features = ["test-support"] } | 101 | rpc = { workspace = true, features = ["test-support"] } |
| 95 | settings = { workspace = true, features = ["test-support"] } | 102 | settings = { workspace = true, features = ["test-support"] } |
| 96 | unindent.workspace = true | 103 | unindent.workspace = true |
| 97 | util = { workspace = true, features = ["test-support"] } | 104 | util = { workspace = true, features = ["test-support"] } |
| 105 | worktree = { workspace = true, features = ["test-support"] } | ||
| @@ -85,6 +85,10 @@ enum OpenBuffer { · 84 unmodified lines | |||
| 85 | 85 | ||
| 86 | pub enum BufferStoreEvent { | 86 | pub 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())?; |
| @@ -0,0 +1,19 @@ | |||
| 1 | //! Zed's debugger data layer is implemented in terms of 3 concepts: | ||
| 2 | //! - DAP store - that knows about all of the available debug sessions. | ||
| 3 | //! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session. | ||
| 4 | //! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host). | ||
| 5 | //! - Breakpoint store - that knows about all breakpoints set for a project. | ||
| 6 | //! | ||
| 7 | //! There are few reasons for this divide: | ||
| 8 | //! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them | ||
| 9 | //! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. | ||
| 10 | //! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain | ||
| 11 | //! current set of breakpoints. | ||
| 12 | //! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. | ||
| 13 | |||
| 14 | pub mod breakpoint_store; | ||
| 15 | pub mod dap_command; | ||
| 16 | pub mod dap_store; | ||
| 17 | pub mod session; | ||
| 18 | |||
| 19 | pub use dap_adapters::attach_processes; | ||
| @@ -0,0 +1,350 @@ | |||
| 1 | # Debugger | ||
| 2 | |||
| 3 | Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages. | ||
| 4 | DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other. | ||
| 5 | It allows Zed to support various debuggers without needing to implement language-specific debugging logic. | ||
| 6 | This protocol enables features like setting breakpoints, stepping through code, inspecting variables, | ||
| 7 | and more, in a consistent manner across different programming languages and runtime environments. | ||
| 8 | |||
| 9 | ## Supported Debug Adapters | ||
| 10 | |||
| 11 | Zed supports a variety of debug adapters for different programming languages: | ||
| 12 | |||
| 13 | - JavaScript (node): Enables debugging of Node.js applications, including setting breakpoints, stepping through code, and inspecting variables in JavaScript. | ||
| 14 | |||
| 15 | - Python (debugpy): Provides debugging capabilities for Python applications, supporting features like remote debugging, multi-threaded debugging, and Django/Flask application debugging. | ||
| 16 | |||
| 17 | - LLDB: A powerful debugger for C, C++, Objective-C, and Swift, offering low-level debugging features and support for Apple platforms. | ||
| 18 | |||
| 19 | - GDB: The GNU Debugger, which supports debugging for multiple programming languages including C, C++, Go, and Rust, across various platforms. | ||
| 20 | |||
| 21 | - Go (dlv): Delve, a debugger for the Go programming language, offering both local and remote debugging capabilities with full support for Go's runtime and standard library. | ||
| 22 | |||
| 23 | - PHP (xdebug): Provides debugging and profiling capabilities for PHP applications, including remote debugging and code coverage analysis. | ||
| 24 | |||
| 25 | - Custom: Allows you to configure any debug adapter that supports the Debug Adapter Protocol, enabling debugging for additional languages or specialized environments not natively supported by Zed. | ||
| 26 | |||
| 27 | These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger. | ||
| 28 | |||
| 29 | ## How To Get Started | ||
| 30 | |||
| 31 | To start a debug session, we added few default debug configurations for each supported language that supports generic configuration options. To see all the available debug configurations, you can use the command palette `debugger: start` action, this should list all the available debug configurations. | ||
| 32 | |||
| 33 | ### Configuration | ||
| 34 | |||
| 35 | To create a custom debug configuration you have to create a `.zed/debug.json` file in your project root directory. This file should contain an array of debug configurations, each with a unique label and adapter the other option are optional/required based on the adapter. | ||
| 36 | |||
| 37 | ```json | ||
| 38 | [ | ||
| 39 | { | ||
| 40 | // The label for the debug configuration and used to identify the debug session inside the debug panel | ||
| 41 | "label": "Example Start debugger config" | ||
| 42 | // The debug adapter that Zed should use to debug the program | ||
| 43 | "adapter": "custom", | ||
| 44 | // Request: defaults to launch | ||
| 45 | // - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration | ||
| 46 | // - attach: Zed will attach to a running program to debug it or when the process_id is not specified we will show a process picker (only supported for node currently) | ||
| 47 | "request": "launch", | ||
| 48 | // cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT) | ||
| 49 | // this field also supports task variables e.g. $ZED_WORKTREE_ROOT | ||
| 50 | "cwd": "$ZED_WORKTREE_ROOT", | ||
| 51 | // program: The program that you want to debug | ||
| 52 | // this fields also support task variables e.g. $ZED_FILE | ||
| 53 | // Note: this field should only contain the path to the program you want to debug | ||
| 54 | "program": "path_to_program", | ||
| 55 | // initialize_args: This field should contain all the adapter specific initialization arguments that are directly send to the debug adapter | ||
| 56 | "initialize_args": { | ||
| 57 | // "stopOnEntry": true // e.g. to stop on the first line of the program (These args are DAP specific) | ||
| 58 | } | ||
| 59 | } | ||
| 60 | ] | ||
| 61 | ``` | ||
| 62 | |||
| 63 | ### Using Attach [WIP] | ||
| 64 | |||
| 65 | Only javascript and lldb supports starting a debug session using attach. | ||
| 66 | |||
| 67 | When using the attach request with a process ID the syntax is as follows: | ||
| 68 | |||
| 69 | ```json | ||
| 70 | { | ||
| 71 | "label": "Attach to Process", | ||
| 72 | "adapter": "javascript", | ||
| 73 | "request": { | ||
| 74 | "attach": { | ||
| 75 | "process_id": "12345" | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | ``` | ||
| 80 | |||
| 81 | Without process ID the syntax is as follows: | ||
| 82 | |||
| 83 | ```json | ||
| 84 | { | ||
| 85 | "label": "Attach to Process", | ||
| 86 | "adapter": "javascript", | ||
| 87 | "request": { | ||
| 88 | "attach": {} | ||
| 89 | } | ||
| 90 | } | ||
| 91 | ``` | ||
| 92 | |||
| 93 | #### JavaScript Configuration | ||
| 94 | |||
| 95 | ##### Debug Active File | ||
| 96 | |||
| 97 | This configuration allows you to debug a JavaScript file in your project. | ||
| 98 | |||
| 99 | ```json | ||
| 100 | { | ||
| 101 | "label": "JavaScript: Debug Active File", | ||
| 102 | "adapter": "javascript", | ||
| 103 | "program": "$ZED_FILE", | ||
| 104 | "request": "launch", | ||
| 105 | "cwd": "$ZED_WORKTREE_ROOT" | ||
| 106 | } | ||
| 107 | ``` | ||
| 108 | |||
| 109 | ##### Debug Terminal | ||
| 110 | |||
| 111 | This configuration will spawn a debug terminal where you could start you program by typing `node test.js`, and the debug adapter will automatically attach to the process. | ||
| 112 | |||
| 113 | ```json | ||
| 114 | { | ||
| 115 | "label": "JavaScript: Debug Terminal", | ||
| 116 | "adapter": "javascript", | ||
| 117 | "request": "launch", | ||
| 118 | "cwd": "$ZED_WORKTREE_ROOT", | ||
| 119 | // "program": "$ZED_FILE", // optional if you pass this in, you will see the output inside the terminal itself | ||
| 120 | "initialize_args": { | ||
| 121 | "console": "integratedTerminal" | ||
| 122 | } | ||
| 123 | } | ||
| 124 | ``` | ||
| 125 | |||
| 126 | #### PHP Configuration | ||
| 127 | |||
| 128 | ##### Debug Active File | ||
| 129 | |||
| 130 | This configuration allows you to debug a PHP file in your project. | ||
| 131 | |||
| 132 | ```json | ||
| 133 | { | ||
| 134 | "label": "PHP: Debug Active File", | ||
| 135 | "adapter": "php", | ||
| 136 | "program": "$ZED_FILE", | ||
| 137 | "request": "launch", | ||
| 138 | "cwd": "$ZED_WORKTREE_ROOT" | ||
| 139 | } | ||
| 140 | ``` | ||
| 141 | |||
209 more lines aren’t shown here.
This diff is large: its 613 lines aren’t shown here.
This diff is large: its 1739 lines aren’t shown here.
This diff is large: its 883 lines aren’t shown here.
This diff is large: its 1763 lines aren’t shown here.
This diff is large: its 46 lines aren’t shown here.
This diff is large: its 435 lines aren’t shown here.
This diff is large: its 113 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 128 lines aren’t shown here.
This diff is large: its 120 lines aren’t shown here.
This diff is large: its 75 lines aren’t shown here.
This diff is large: its 10 lines aren’t shown here.
This diff is large: its 586 lines aren’t shown here.
This diff is large: its 312 lines aren’t shown here.
This diff is large: its 12 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 91 lines aren’t shown here.
This diff is large: its 17 lines aren’t shown here.
This diff is large: its 27 lines aren’t shown here.
This diff is large: its 15 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 27 lines aren’t shown here.
This diff is large: its 228 lines aren’t shown here.
This diff is large: its 61 lines aren’t shown here.
This diff is large: its 136 lines aren’t shown here.
This diff is large: its 12 lines aren’t shown here.
This diff is large: its 174 lines aren’t shown here.
This diff is large: its 81 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 19 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 21 lines aren’t shown here.
This diff is large: its 63 lines aren’t shown here.
This diff is large: its 37 lines aren’t shown here.
This diff is large: its 75 lines aren’t shown here.
This diff is large: its 23 lines aren’t shown here.
This diff is large: its 13 lines aren’t shown here.
This diff is large: its 20 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 17 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 80 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 87 lines aren’t shown here.
This diff is large: its 496 lines aren’t shown here.
This diff is large: its 21 lines aren’t shown here.
This diff is large: its 90 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 227 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.